Browse Source

feat: Playlists can now contain URLs

Matthias Ladkau 4 years ago
parent
commit
523ac35352
2 changed files with 129 additions and 24 deletions
  1. 52 22
      playlist/fileplaylist.go
  2. 77 2
      playlist/fileplaylist_test.go

+ 52 - 22
playlist/fileplaylist.go

@@ -21,22 +21,26 @@ a file. The definition file is expected to be a JSON encoded datastructure of th
 	        {
 	            "artist" : <artist>
 	            "title"  : <title>
-	            "path"   : <file path>
+	            "path"   : <file path / url>
 	        }
 	    ]
 	}
 
 The web path is the absolute path which may be requested by the streaming
 client (e.g. /foo/bar would be http://myserver:1234/foo/bar).
-The file path is a physical file reachable by the server process. The file
-ending determines the content type which is send to the client.
+The path is either a physical file or a web url reachable by the server process.
+The file ending determines the content type which is send to the client.
 */
 package playlist
 
 import (
+	"crypto/tls"
 	"encoding/json"
+	"io"
 	"io/ioutil"
 	"math/rand"
+	"net/http"
+	"net/url"
 	"os"
 	"path/filepath"
 	"sync"
@@ -92,15 +96,21 @@ func NewFilePlaylistFactory(path string) (*FilePlaylistFactory, error) {
 		return nil, err
 	}
 
-	// Strip out comments
-
-	pl = stringutil.StripCStyleComments(pl)
-
 	// Unmarshal json
 
 	ret := &FilePlaylistFactory{}
 
 	err = json.Unmarshal(pl, &ret.data)
+
+	if err != nil {
+
+		// Try again and strip out comments
+
+		pl = stringutil.StripCStyleComments(pl)
+
+		err = json.Unmarshal(pl, &ret.data)
+	}
+
 	if err != nil {
 		return nil, err
 	}
@@ -141,7 +151,7 @@ type FilePlaylist struct {
 	path      string              // Path of this playlist
 	current   int                 // Pointer to the current playing item
 	data      []map[string]string // Playlist items
-	file      *os.File            // Current open file
+	stream    io.ReadCloser       // Current open stream
 	finished  bool                // Flag if this playlist has finished
 	framePool *sync.Pool          // Pool for byte arrays
 }
@@ -202,7 +212,7 @@ func (fp *FilePlaylist) Frame() ([]byte, error) {
 		return nil, dudeldu.ErrPlaylistEnd
 	}
 
-	if fp.file == nil {
+	if fp.stream == nil {
 
 		// Make sure first file is loaded
 
@@ -220,13 +230,13 @@ func (fp *FilePlaylist) Frame() ([]byte, error) {
 
 		for n < len(frame) && err == nil {
 
-			nn, err = fp.file.Read(frame[n:])
+			nn, err = fp.stream.Read(frame[n:])
 
 			n += nn
 
 			// Check if we need to read the next file
 
-			if n < len(frame) {
+			if n < len(frame) || err == io.EOF {
 				err = fp.nextFile()
 			}
 		}
@@ -261,14 +271,16 @@ func (fp *FilePlaylist) Frame() ([]byte, error) {
 nextFile jumps to the next file for the playlist.
 */
 func (fp *FilePlaylist) nextFile() error {
+	var err error
+	var stream io.ReadCloser
 
 	// Except for the first call advance the current pointer
 
-	if fp.file != nil {
+	if fp.stream != nil {
 		fp.current++
 
-		fp.file.Close()
-		fp.file = nil
+		fp.stream.Close()
+		fp.stream = nil
 
 		// Return special error if the end of the playlist has been reached
 
@@ -279,11 +291,29 @@ func (fp *FilePlaylist) nextFile() error {
 
 	// Check if a file is already open
 
-	if fp.file == nil {
+	if fp.stream == nil {
 
-		// Open a new file
+		item := fp.currentItem()["path"]
+
+		if _, err = url.ParseRequestURI(item); err == nil {
+			var resp *http.Response
+
+			// We got an url - access it without SSL verification
+
+			client := &http.Client{Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			}}
+
+			resp, err = client.Get(item)
+			stream = resp.Body
+
+		} else {
+
+			// Open a new file
+
+			stream, err = os.Open(item)
+		}
 
-		f, err := os.Open(fp.currentItem()["path"])
 		if err != nil {
 
 			// Jump to the next file if there is an error
@@ -293,10 +323,10 @@ func (fp *FilePlaylist) nextFile() error {
 			return err
 		}
 
-		fp.file = f
+		fp.stream = stream
 	}
 
-	return nil
+	return err
 }
 
 /*
@@ -320,9 +350,9 @@ Close any open files by this playlist and reset the current pointer. After this
 call the playlist can be played again.
 */
 func (fp *FilePlaylist) Close() error {
-	if fp.file != nil {
-		fp.file.Close()
-		fp.file = nil
+	if fp.stream != nil {
+		fp.stream.Close()
+		fp.stream = nil
 	}
 	fp.current = 0
 	fp.finished = false

+ 77 - 2
playlist/fileplaylist_test.go

@@ -14,14 +14,18 @@ import (
 	"flag"
 	"fmt"
 	"io/ioutil"
+	"net/http"
 	"os"
 	"sync"
 	"testing"
 
 	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/common/httputil"
 	"devt.de/krotik/dudeldu"
 )
 
+const TESTPORT = ":9092"
+
 const pdir = "playlisttest"
 
 const testPlaylist = `
@@ -69,6 +73,11 @@ const testPlaylist2 = `{
 			"artist" : "artist3",
 			"title"  : "test3",
 			"path"   : "playlisttest/test3.xyz"
+		},
+		{
+			"artist" : "artist4",
+			"title"  : "test4",
+			"path"   : "http://localhost:9092/songs/song1.mp3"
 		}
 	]
 }`
@@ -105,6 +114,18 @@ func TestFilePlaylist(t *testing.T) {
 
 	// Set up
 
+	hs, wg := startServer()
+	if hs == nil {
+		return
+	}
+	defer func() {
+		stopServer(hs, wg)
+	}()
+
+	http.HandleFunc("/songs/song1.mp3", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("songdata123"))
+	})
+
 	err := ioutil.WriteFile(pdir+"/test1.json", []byte(testPlaylist), 0644)
 	if err != nil {
 		t.Error(err)
@@ -479,17 +500,28 @@ func TestFilePlaylist(t *testing.T) {
 	}
 
 	frame, err = pl2.Frame()
+
+	if err != nil {
+		t.Error(err)
+		return
+	} else if string(frame) != "Asongd" {
+		t.Error("Unexpected frame:", string(frame), frame)
+		return
+	}
+
+	frame, err = pl2.Frame()
+
 	if err != dudeldu.ErrPlaylistEnd {
 		t.Error(err)
 		return
-	} else if string(frame) != "A" {
+	} else if string(frame) != "ata123" {
 		t.Error("Unexpected frame:", string(frame), frame)
 		return
 	}
 
 	// Make sure currentItem does not blow up
 
-	if pl2.Title() != "test3" {
+	if pl2.Title() != "test4" {
 		t.Error("Unexpected result:", pl2.Title())
 		return
 	}
@@ -503,3 +535,46 @@ func TestFilePlaylist(t *testing.T) {
 		return
 	}
 }
+
+/*
+Start a HTTP test server.
+*/
+func startServer() (*httputil.HTTPServer, *sync.WaitGroup) {
+	hs := &httputil.HTTPServer{}
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	go hs.RunHTTPServer(TESTPORT, &wg)
+
+	wg.Wait()
+
+	// Server is started
+
+	if hs.LastError != nil {
+		panic(hs.LastError)
+	}
+
+	return hs, &wg
+}
+
+/*
+Stop a started HTTP test server.
+*/
+func stopServer(hs *httputil.HTTPServer, wg *sync.WaitGroup) {
+
+	if hs.Running == true {
+
+		wg.Add(1)
+
+		// Server is shut down
+
+		hs.Shutdown()
+
+		wg.Wait()
+
+	} else {
+
+		panic("Server was not running as expected")
+	}
+}