-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement support for .pls and .m3u -playlists (#180)
* implement .pls support * application: change pls to use QueueLoadItems * move playlist parsing into separate module
- Loading branch information
Showing
10 changed files
with
362 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Oops, something went wrong.