Commit 85a1ffca authored by Christian Pointner's avatar Christian Pointner
Browse files

improved sanity chekcs for playlists, add duration field to playlist entries

parent 46b373a9
Pipeline #722 failed with stages
in 20 minutes and 54 seconds
......@@ -71,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
},
},
}
)
......
......@@ -38,33 +38,71 @@ func generateFileURI(file *File) string {
return uri.String()
}
func (p *Playlist) BeforeSave() error {
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 {
if p.Entries[idx].URI != "" && p.Entries[idx].URI != generateFileURI(p.Entries[idx].File) {
return ErrInvalidPlaylistEntry{idx, "File and Uri Parameter mismatch"}
if p.Entries[idx].File.ShowName == "" {
p.Entries[idx].File.ShowName = p.ShowName
}
} else if p.Entries[idx].URI != "" {
uri, err := url.Parse(p.Entries[idx].URI)
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 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 ErrInvalidPlaylistEntry{idx, err.Error()}
}
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 ErrInvalidPlaylistEntry{idx, "entries must either contain a File or have a URI set"}
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
}
......@@ -76,6 +114,7 @@ func (p *Playlist) AfterFind() error {
for idx := range p.Entries {
if p.Entries[idx].File != nil {
p.Entries[idx].URI = generateFileURI(p.Entries[idx].File)
p.Entries[idx].Duration = &p.Entries[idx].File.Duration
}
}
return 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 {
......@@ -208,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