Skip to content

Commit

Permalink
Merge branch 'master' into scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
vishen committed Mar 14, 2024
2 parents c90081e + 9cc0bed commit 296236d
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 4 deletions.
46 changes: 45 additions & 1 deletion application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import (

"github.com/vishen/go-chromecast/cast"
pb "github.com/vishen/go-chromecast/cast/proto"
"github.com/vishen/go-chromecast/playlists"
"github.com/vishen/go-chromecast/storage"
"gopkg.in/ini.v1"
"path/filepath"
)

var (
Expand Down Expand Up @@ -764,6 +767,35 @@ func (a *Application) PlayedItems() map[string]PlayedItem {
}

func (a *Application) Load(filenameOrUrl string, startTime int, contentType string, transcode, detach, forceDetach bool) error {
// if the file is a playlist, ".pls", then just play the first item.
if playlists.IsPlaylist(filenameOrUrl) {
if strings.HasPrefix(filenameOrUrl, "./") { // convert to file:// uri
if abs, err := filepath.Abs(filenameOrUrl); err != nil {
return err
} else {
filenameOrUrl = fmt.Sprintf("file://%v", abs)
}
}
it, err := playlists.NewIterator(filenameOrUrl)
if err != nil {
return err
}
var items []mediaItem
for it.HasNext() {
url, title := it.Next()
items = append(items, mediaItem{
filename: url,
contentURL: url,
})
fmt.Printf("Adding url %v (%v)\n", url, title)
}
return a.QueueLoadItems(items, "")
}
return a.play(filenameOrUrl, startTime, contentType, transcode, detach, forceDetach)
}

func (a *Application) play(filenameOrUrl string, startTime int, contentType string, transcode, detach, forceDetach bool) error {

var mi mediaItem
isExternalMedia := false
if strings.HasPrefix(filenameOrUrl, "http://") || strings.HasPrefix(filenameOrUrl, "https://") {
Expand Down Expand Up @@ -850,11 +882,14 @@ func (a *Application) LoadApp(appID, contentID string) error {
}

func (a *Application) QueueLoad(filenames []string, contentType string, transcode bool) error {

mediaItems, err := a.loadAndServeFiles(filenames, contentType, transcode)
if err != nil {
return errors.Wrap(err, "unable to load and serve files")
}
return a.QueueLoadItems(mediaItems, contentType)
}

func (a *Application) QueueLoadItems(mediaItems []mediaItem, contentType string) error {

if err := a.ensureIsDefaultMediaReceiver(); err != nil {
return err
Expand Down Expand Up @@ -1392,3 +1427,12 @@ func (a *Application) Transcode(contentType string, command string, args ...stri
a.MediaWait()
return nil
}

// plsIterator is an iterator for playlist-files.
// According to https://en.wikipedia.org/wiki/PLS_(file_format),
// The format is case-sensitive and essentially that of an INI file.
// It has entries on the form File1, Title1 etc.
type plsIterator struct {
count int
playlist *ini.Section
}
31 changes: 31 additions & 0 deletions application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"encoding/json"
"testing"

"fmt"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/vishen/go-chromecast/application"
"github.com/vishen/go-chromecast/cast"
mockCast "github.com/vishen/go-chromecast/cast/mocks"
pb "github.com/vishen/go-chromecast/cast/proto"
"path/filepath"
)

var mockAddr = "foo.bar"
Expand Down Expand Up @@ -42,3 +44,32 @@ func TestApplicationStart(t *testing.T) {
app := application.NewApplication(application.WithConnection(conn))
assertions.NoError(app.Start(mockAddr, mockPort))
}

func TestParsePlaylist(t *testing.T) {
var path string
if abs, err := filepath.Abs(filepath.Join("..", "testdata", "indiepop64.pls")); err != nil {
t.Fatal(err)
} else {
path = fmt.Sprintf("file://%v", abs)
}
it, err := application.NewPlaylistIterator(path)

Check failure on line 55 in application/application_test.go

View workflow job for this annotation

GitHub Actions / test (1.21.x, ubuntu-latest)

undefined: application.NewPlaylistIterator

Check failure on line 55 in application/application_test.go

View workflow job for this annotation

GitHub Actions / test (1.21.x, macos-latest)

undefined: application.NewPlaylistIterator
if err != nil {
t.Fatal(err)
}
var wantUrls = []string{
"https://ice4.somafm.com/indiepop-64-aac",
"https://ice2.somafm.com/indiepop-64-aac",
"https://ice1.somafm.com/indiepop-64-aac",
"https://ice6.somafm.com/indiepop-64-aac",
"https://ice5.somafm.com/indiepop-64-aac",
}
for i, want := range wantUrls {
if !it.HasNext() {
t.Fatal("iterator exhausted")
}
have, _ := it.Next()
if have != want {
t.Fatalf("url %d, have %v want %v", i, have, want)
}
}
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
)

require github.com/seancfoley/ipaddress-go v1.5.5
require gopkg.in/ini.v1 v1.67.0

require (
cloud.google.com/go v0.110.0 // indirect
Expand Down Expand Up @@ -52,7 +53,7 @@ require (
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/grpc v1.56.3 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/errgo.v2 v2.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,15 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
83 changes: 83 additions & 0 deletions playlists/playlist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package playlists

import (
"fmt"
"path/filepath"
"testing"
)

func TestParsePLSFormat(t *testing.T) {
var wantUrls = []struct {
url string
title string
}{
{"https://ice4.somafm.com/indiepop-64-aac", "SomaFM: Indie Pop Rocks! (#1): New and classic favorite indie pop tracks."},
{"https://ice2.somafm.com/indiepop-64-aac", "SomaFM: Indie Pop Rocks! (#2): New and classic favorite indie pop tracks."},
{"https://ice1.somafm.com/indiepop-64-aac", "SomaFM: Indie Pop Rocks! (#3): New and classic favorite indie pop tracks."},
{"https://ice6.somafm.com/indiepop-64-aac", "SomaFM: Indie Pop Rocks! (#4): New and classic favorite indie pop tracks."},
{"https://ice5.somafm.com/indiepop-64-aac", "SomaFM: Indie Pop Rocks! (#5): New and classic favorite indie pop tracks."},
}
var path string
if abs, err := filepath.Abs(filepath.Join("testdata", "indiepop64.pls")); err != nil {
t.Fatal(err)
} else {
path = fmt.Sprintf("file://%v", abs)
}
it, err := newPLSIterator(path)
if err != nil {
t.Fatal(err)
}
for i, want := range wantUrls {
if !it.HasNext() {
t.Fatal("iterator exhausted")
}
url, title := it.Next()
if url != want.url {
t.Fatalf("test %d, have url %v want %v", i, url, want.url)
}
if title != want.title {
t.Fatalf("test %d, have title %v want %v", i, title, want.title)
}
}
if it.HasNext() {
t.Fatal("expected exhausted iterator")
}
}

func TestParseM3UFormat(t *testing.T) {
var wantUrls = []struct {
url string
title string
}{
{"http://ice1.somafm.com/indiepop-128-aac", ""},
{"http://ice4.somafm.com/indiepop-128-aac", ""},
{"http://ice2.somafm.com/indiepop-128-aac", ""},
{"http://ice6.somafm.com/indiepop-128-aac", ""},
{"http://ice5.somafm.com/indiepop-128-aac", ""},
}
var path string
if abs, err := filepath.Abs(filepath.Join("testdata", "indiepop130.m3u")); err != nil {
t.Fatal(err)
} else {
path = fmt.Sprintf("file://%v", abs)
}
it, err := newM3UIterator(path)
if err != nil {
t.Fatal(err)
}
for i, want := range wantUrls {
if !it.HasNext() {
t.Fatal("iterator exhausted")
}
url, title := it.Next()
if url != want.url {
t.Fatalf("test %d, have url %v want %v", i, url, want.url)
}
if title != want.title {
t.Fatalf("test %d, have title %v want %v", i, title, want.title)
}
}
if it.HasNext() {
t.Fatal("expected exhausted iterator")
}
}
122 changes: 122 additions & 0 deletions playlists/playlists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package playlists

import (
"fmt"
"gopkg.in/ini.v1"
"path/filepath"
"strings"
)

type Iterator interface {
HasNext() bool
Next() (file, title string)
}

func IsPlaylist(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
return ext == ".m3u" || ext == ".pls"
}

// NewPlaylistIterator creates an iterator for the given playlist.
func NewIterator(uri string) (Iterator, error) {
ext := strings.ToLower(filepath.Ext(uri))
switch ext {
case ".pls":
return newPLSIterator(uri)
case ".m3u":
return newM3UIterator(uri)
}
return nil, fmt.Errorf("'%v' is not a recognized playlist format", ext)
}

// plsIterator is an iterator for playlist-files.
// According to https://en.wikipedia.org/wiki/PLS_(file_format),
// The format is case-sensitive and essentially that of an INI file.
// It has entries on the form File1, Title1 etc.
type plsIterator struct {
count int
playlist *ini.Section
}

func newPLSIterator(uri string) (*plsIterator, error) {
content, err := FetchResource(uri)
if err != nil {
return nil, fmt.Errorf("failed to read file %v: %w", uri, err)
}
pls, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, content)
if err != nil {
return nil, fmt.Errorf("failed to parse file %v: %w", uri, err)
}
section, err := pls.GetSection("playlist")
if err != nil {
return nil, fmt.Errorf("failed to find playlist in .pls-file %v", uri)
}
return &plsIterator{
playlist: section,
}, nil
}

func (it *plsIterator) HasNext() bool {
return it.playlist.HasKey(fmt.Sprintf("File%d", it.count+1))
}

func (it *plsIterator) Next() (file, title string) {
if val := it.playlist.Key(fmt.Sprintf("File%d", it.count+1)); val != nil {
file = val.Value()
}
if val := it.playlist.Key(fmt.Sprintf("Title%d", it.count+1)); val != nil {
title = val.Value()
}
it.count = it.count + 1
return file, title
}

// m3uIterator is an iterator for m3u-files.
// https://docs.fileformat.com/audio/m3u/:
//
// There is no official specification for the M3U file format, it is a de-facto standard.
// M3U is a plain text file that uses the .m3u extension if the text is encoded
// in the local system’s default non-Unicode encoding or with the .m3u8 extension
// if the text is UTF-8 encoded. Each entry in the M3U file can be one of the following:
//
// - Absolute path to the file
// - File path relative to the M3U file.
// - URL
//
// In the extended M3U, additional directives are introduced that begin
// with “#” and end with a colon(:) if they have parameters
type m3uIterator struct {
index int
lines []string
}

func newM3UIterator(uri string) (*m3uIterator, error) {
content, err := FetchResource(uri)
if err != nil {
return nil, fmt.Errorf("failed to read file %v: %w", uri, err)
}
var lines []string
// convert windows linebreaks, and split
for _, l := range strings.Split(strings.ReplaceAll(string(content), "\r\n", "\n"), "\n") {
// This is a very simple m3u decoder, ignores all extended info
l = strings.TrimSpace(l)
if len(l) > 0 && !strings.HasPrefix(l, "#") {
lines = append(lines, l)
}
}
return &m3uIterator{
index: 0,
lines: lines,
}, nil
}

func (it *m3uIterator) HasNext() bool {
return it.index < len(it.lines)
}

func (it *m3uIterator) Next() (file, title string) {
file = it.lines[it.index]
title = "" // Todo?
it.index++
return file, title
}
13 changes: 13 additions & 0 deletions playlists/testdata/indiepop130.m3u
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#EXTM3U
#EXTINF:-1,SomaFM - Indie Pop Rocks!
http://ice1.somafm.com/indiepop-128-aac
#EXTINF:-1,SomaFM - Indie Pop Rocks!
http://ice4.somafm.com/indiepop-128-aac
#EXTINF:-1,SomaFM - Indie Pop Rocks!
http://ice2.somafm.com/indiepop-128-aac
#EXTINF:-1,SomaFM - Indie Pop Rocks!
http://ice6.somafm.com/indiepop-128-aac
#EXTINF:-1,SomaFM - Indie Pop Rocks!
http://ice5.somafm.com/indiepop-128-aac


19 changes: 19 additions & 0 deletions playlists/testdata/indiepop64.pls
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[playlist]
numberofentries=5
File1=https://ice4.somafm.com/indiepop-64-aac
Title1=SomaFM: Indie Pop Rocks! (#1): New and classic favorite indie pop tracks.
Length1=-1
File2=https://ice2.somafm.com/indiepop-64-aac
Title2=SomaFM: Indie Pop Rocks! (#2): New and classic favorite indie pop tracks.
Length2=-1
File3=https://ice1.somafm.com/indiepop-64-aac
Title3=SomaFM: Indie Pop Rocks! (#3): New and classic favorite indie pop tracks.
Length3=-1
File4=https://ice6.somafm.com/indiepop-64-aac
Title4=SomaFM: Indie Pop Rocks! (#4): New and classic favorite indie pop tracks.
Length4=-1
File5=https://ice5.somafm.com/indiepop-64-aac
Title5=SomaFM: Indie Pop Rocks! (#5): New and classic favorite indie pop tracks.
Length5=-1
Version=2

Loading

0 comments on commit 296236d

Please sign in to comment.