Commit 8f067adf authored by Christian Pointner's avatar Christian Pointner
Browse files

Merge branch 'topic/playlist-entry-durations'

parents 52f14a64 0f16221a
Pipeline #724 failed with stages
in 5 minutes and 52 seconds
......@@ -48,6 +48,8 @@ func statusCodeFromError(err error) (code int, response ErrorResponse) {
response.Detail = err
case store.ErrInvalidMetadataField:
code = http.StatusBadRequest
case store.ErrInvalidPlaylistEntry:
code = http.StatusBadRequest
case *importer.JobSourceResult:
response.Detail = err.(*importer.JobSourceResult).Log
default:
......@@ -69,6 +71,8 @@ func statusCodeFromError(err error) (code int, response ErrorResponse) {
code = http.StatusConflict
case store.ErrFileImportNotDone:
code = http.StatusConflict
case store.ErrPlaylistHasMultipleNullDurationEntries:
code = http.StatusBadRequest
case importer.ErrNotImplemented:
code = http.StatusNotImplemented
......
......@@ -217,6 +217,30 @@ var (
return tx.Table("files").DropColumn("metadata__isrc").Error
},
},
{
ID: "202006130203",
Migrate: func(tx *gorm.DB) error {
type PlaylistEntry struct {
ID uint64 `json:"-" gorm:"primary_key"`
PlaylistID uint64 `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
LineNum uint `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
URI string `json:"uri" gorm:"size:1024"`
Duration *time.Duration `json:"duration,omitempty"`
File *File `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
FileID *uint64 `json:"-" gorm:"index"`
}
// actually all playlists would need to be verfied if they still fit the new contstraint
// that only allows a sinle non-file entry with durtion == NULL per playlist.
// However we are still pre-first-release and all migrations will likely be squashed before that
// release anyway...
return tx.AutoMigrate(&PlaylistEntry{}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Table("playlist_entries").DropColumn("duration").Error
},
},
}
)
......
......@@ -33,31 +33,76 @@ import (
"github.com/jinzhu/gorm"
)
func (p *Playlist) BeforeSave() error {
func generateFileURI(file *File) string {
uri := url.URL{Scheme: FileURIScheme, Host: file.ShowName, Path: strconv.FormatUint(file.ID, 10)}
return uri.String()
}
func (p *Playlist) BeforeSave(tx *gorm.DB) error {
referencedFiles := make(map[uint64]*File)
hasEntryWithoutDuration := false
for idx := range p.Entries {
p.Entries[idx].LineNum = uint(idx)
if p.Entries[idx].File != nil {
p.Entries[idx].URI = "" // this will be regenerated in AfterFind()
} else if p.Entries[idx].URI != "" {
uri, err := url.Parse(p.Entries[idx].URI)
if p.Entries[idx].File.ShowName == "" {
p.Entries[idx].File.ShowName = p.ShowName
}
referencedFiles[p.Entries[idx].File.ID] = nil
continue
}
if p.Entries[idx].URI == "" {
return ErrInvalidPlaylistEntry{idx, "entries must either reference a File or have a URI set"}
}
uri, err := url.Parse(p.Entries[idx].URI)
if err != nil {
return ErrInvalidPlaylistEntry{idx, err.Error()}
}
if uri.Scheme == FileURIScheme {
if uri.Host == "" || uri.Path == "" {
return ErrInvalidPlaylistEntry{idx, "uri must be in the format " + FileURIScheme + "://<show>/<file-id>"}
}
fileID, err := strconv.ParseUint(strings.TrimPrefix(uri.Path, "/"), 10, 64)
if err != nil {
return fmt.Errorf("playlist entry #%d is invalid: %v", idx, err)
return ErrInvalidPlaylistEntry{idx, err.Error()}
}
if uri.Scheme == FileURIScheme {
if uri.Host == "" || uri.Path == "" {
return fmt.Errorf("playlist entry #%d is invalid: uri must be in the format %s://<show>/<file-id>", idx, FileURIScheme)
}
fileID, err := strconv.ParseUint(strings.TrimPrefix(uri.Path, "/"), 10, 64)
if err != nil {
return fmt.Errorf("playlist entry #%d is invalid: %v", idx, err)
}
p.Entries[idx].File = &File{ID: fileID, ShowName: uri.Host}
p.Entries[idx].URI = "" // this will be regenerated in AfterFind()
p.Entries[idx].File = &File{ID: fileID, ShowName: uri.Host}
referencedFiles[fileID] = nil
p.Entries[idx].URI = "" // this will be regenerated in AfterFind()
} else if p.Entries[idx].Duration == nil {
if hasEntryWithoutDuration {
return ErrPlaylistHasMultipleNullDurationEntries
}
} else {
return fmt.Errorf("playlist entry #%d is invalid, entries must either contain a File or have a URI set", idx)
hasEntryWithoutDuration = true
}
}
var fileEntryIDs []uint64
for id, _ := range referencedFiles {
fileEntryIDs = append(fileEntryIDs, id)
}
var files []*File
if err := tx.Where("id IN (?)", fileEntryIDs).Model(&File{}).Select("id, show_name, duration").Scan(&files).Error; err != nil {
return nil
}
for _, file := range files {
referencedFiles[file.ID] = file
}
for idx := range p.Entries {
if p.Entries[idx].File == nil {
continue
}
if referencedFiles[p.Entries[idx].File.ID] == nil || referencedFiles[p.Entries[idx].File.ID].ShowName != p.Entries[idx].File.ShowName {
return ErrInvalidPlaylistEntry{idx, fmt.Sprintf("file '%s/%d' does not exist", p.Entries[idx].File.ShowName, p.Entries[idx].File.ID)}
}
if p.Entries[idx].Duration != nil && referencedFiles[p.Entries[idx].File.ID].Duration != *(p.Entries[idx].Duration) {
return ErrInvalidPlaylistEntry{idx, "provided duration and file duration mismatch"}
}
p.Entries[idx].Duration = nil // this will be regenerated in AfterFind()
}
return nil
}
......@@ -68,10 +113,8 @@ func (p *Playlist) AfterSave(tx *gorm.DB) error {
func (p *Playlist) AfterFind() error {
for idx := range p.Entries {
if p.Entries[idx].File != nil {
urihost := p.Entries[idx].File.ShowName
uripath := strconv.FormatUint(p.Entries[idx].File.ID, 10)
uri := url.URL{Scheme: FileURIScheme, Host: urihost, Path: uripath}
p.Entries[idx].URI = uri.String()
p.Entries[idx].URI = generateFileURI(p.Entries[idx].File)
p.Entries[idx].Duration = &p.Entries[idx].File.Duration
}
}
return nil
......
......@@ -32,6 +32,7 @@ import (
"path/filepath"
"reflect"
"testing"
"time"
//"github.com/jinzhu/gorm"
)
......@@ -574,9 +575,14 @@ func TestFilesSourceHash(t *testing.T) {
// Playlists
//
func generateTestPlaylist(uris ...string) (p Playlist) {
for _, u := range uris {
e := PlaylistEntry{URI: u}
type playlistTestEntry struct {
uri string
duration *time.Duration
}
func generateTestPlaylist(entries ...playlistTestEntry) (p Playlist) {
for _, entry := range entries {
e := PlaylistEntry{URI: entry.uri, Duration: entry.duration}
p.Entries = append(p.Entries, e)
}
return p
......@@ -601,8 +607,14 @@ func TestPlaylistsListCreateDelete(t *testing.T) {
t.Fatalf("listing playlists of not existing show should return and empty list but ListPlaylists returned: %v", playlists)
}
in := generateTestPlaylist("audioin://1", "http://stream.example.com/live.mp")
in := generateTestPlaylist(playlistTestEntry{"audioin://1", nil}, playlistTestEntry{"http://stream.example.com/live.mp", nil})
testPlaylist, err := store.CreatePlaylist(testShowName, in)
if err != ErrPlaylistHasMultipleNullDurationEntries {
t.Fatalf("creating playlist with more than one non-file entry without duration should fail with specific error but returned: %v", err)
}
testDuration := time.Second
in = generateTestPlaylist(playlistTestEntry{"audioin://1", &testDuration}, playlistTestEntry{"http://stream.example.com/live.mp", nil})
testPlaylist, err = store.CreatePlaylist(testShowName, in)
if err != nil {
t.Fatalf("creating playlist in test show failed: %v", err)
}
......@@ -615,12 +627,12 @@ func TestPlaylistsListCreateDelete(t *testing.T) {
t.Fatalf("ListPlaylists should return a single playlist but returned: %v", playlists)
}
in1 := generateTestPlaylist("audioin://1", "http://stream.example.com/live.mp3")
in1 := generateTestPlaylist(playlistTestEntry{"audioin://1", &testDuration}, playlistTestEntry{"http://stream.example.com/live.mp3", nil})
_, err = store.CreatePlaylist(testShow1, in1)
if err != nil {
t.Fatalf("creating playlist in not existing show shouldn't throw an error but CreatePlaylist returned: %v", err)
}
in2 := generateTestPlaylist("https://stream.example.com/other.ogg", "audioin://2")
in2 := generateTestPlaylist(playlistTestEntry{"https://stream.example.com/other.ogg", &testDuration}, playlistTestEntry{"audioin://2", nil})
_, err = store.CreatePlaylist(testShow1, in2)
if err != nil {
t.Fatalf("creating playlist in not existing show shouldn't throw an error but CreatePlaylist returned: %v", err)
......@@ -667,7 +679,7 @@ func TestPlaylistsCreateAndGet(t *testing.T) {
t.Fatalf("getting playlist in not-existing show should return ErrNotFound, but GetPlaylist returned: %v", err)
}
p := generateTestPlaylist("http://stream.example.com/stream.mp3")
p := generateTestPlaylist(playlistTestEntry{"http://stream.example.com/stream.mp3", nil})
p.Entries = append(p.Entries, PlaylistEntry{File: &File{ShowName: file1.ShowName, ID: file1.ID}})
list1, err := store.CreatePlaylist(testShow1, p)
if err != nil {
......@@ -679,7 +691,7 @@ func TestPlaylistsCreateAndGet(t *testing.T) {
}
// TODO: check if playlists are equal
p = generateTestPlaylist("http://stream.example.com/other.mp3", fmt.Sprintf("file://%s/%d", file1.ShowName, file1.ID))
p = generateTestPlaylist(playlistTestEntry{"http://stream.example.com/other.mp3", nil}, playlistTestEntry{fmt.Sprintf("file://%s/%d", file1.ShowName, file1.ID), nil})
if _, err = store.CreatePlaylist(testShow1, p); err != nil {
t.Fatalf("unexpected error: %v", err)
}
......@@ -719,7 +731,7 @@ func TestFileUsage(t *testing.T) {
t.Fatalf("file should be in use by any playlist but got %d entries in usage list", len(lists))
}
p := generateTestPlaylist("http://stream.example.com/stream.mp3")
p := generateTestPlaylist(playlistTestEntry{"http://stream.example.com/stream.mp3", nil})
p.Entries = append(p.Entries, PlaylistEntry{File: &File{ShowName: file.ShowName, ID: file.ID}})
list, err := store.CreatePlaylist(testShow1, p)
if err != nil {
......
......@@ -48,6 +48,8 @@ var (
ErrShowAlreadyExists = errors.New("show already exists")
ErrFileNotNew = errors.New("file does not exist or is not new")
ErrFileImportNotDone = errors.New("file import is not done")
ErrPlaylistHasMultipleNullDurationEntries = errors.New("playlists may only have one entry without a duration")
)
type ErrFileInUse struct {
......@@ -64,6 +66,15 @@ func (e ErrInvalidMetadataField) Error() string {
return "invalid metadata field: " + string(e)
}
type ErrInvalidPlaylistEntry struct {
idx int
message string
}
func (e ErrInvalidPlaylistEntry) Error() string {
return fmt.Sprintf("playlist entry #%d is invalid: %s", e.idx, e.message)
}
//******* Logs
type LogLine struct {
......@@ -199,12 +210,13 @@ type ImportLogs map[string]*Log
//******* Playlists
type PlaylistEntry struct {
ID uint64 `json:"-" gorm:"primary_key"`
PlaylistID uint64 `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
LineNum uint `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
URI string `json:"uri" gorm:"size:1024"`
File *File `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
FileID *uint64 `json:"-" gorm:"index"`
ID uint64 `json:"-" gorm:"primary_key"`
PlaylistID uint64 `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
LineNum uint `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
URI string `json:"uri" gorm:"size:1024"`
Duration *time.Duration `json:"duration,omitempty"`
File *File `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
FileID *uint64 `json:"-" gorm:"index"`
}
type Playlist struct {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment