@@ -26,40 +26,6 @@ const (
|
|||||||
extM3U8 = ".m3u8"
|
extM3U8 = ".m3u8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
|
||||||
basePath string
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore(basePath string) (*Store, error) {
|
|
||||||
if basePath == "" {
|
|
||||||
return nil, ErrInvalidBasePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity check layout, just in case someone tries to use an existing folder
|
|
||||||
entries, err := os.ReadDir(basePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sanity checking: reading dir: %w", err)
|
|
||||||
}
|
|
||||||
var found string
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := userIDFromPath(entry.Name()); err != nil {
|
|
||||||
found = entry.Name()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found != "" {
|
|
||||||
return nil, fmt.Errorf("sanity checking: %w: item %q in playlists directory is not a user id. see wiki for details on layout of the playlists dir", ErrNoUserPrefix, found)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Store{
|
|
||||||
basePath: basePath,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Playlist struct {
|
type Playlist struct {
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
UserID int
|
UserID int
|
||||||
@@ -69,18 +35,33 @@ type Playlist struct {
|
|||||||
IsPublic bool
|
IsPublic bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPath(userID int, playlistName string) string {
|
type Store struct {
|
||||||
playlistName = fileutil.Safe(playlistName)
|
basePath string
|
||||||
if playlistName == "" {
|
mu sync.Mutex
|
||||||
playlistName = "pl"
|
}
|
||||||
|
|
||||||
|
func NewStore(basePath string) (*Store, error) {
|
||||||
|
if basePath == "" {
|
||||||
|
return nil, ErrInvalidBasePath
|
||||||
}
|
}
|
||||||
playlistName = fmt.Sprintf("%s-%d%s", playlistName, time.Now().UnixMilli(), extM3U)
|
if err := sanityCheck(basePath); err != nil {
|
||||||
return filepath.Join(fmt.Sprint(userID), playlistName)
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Store{
|
||||||
|
basePath: basePath,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List finds playlist items in s.basePath.
|
// List finds playlist items in s.basePath.
|
||||||
// the expected format is <base path>/<user id>/**/<playlist name>.m3u
|
// the expected format is <base path>/<user id>/**/<playlist name>.m3u
|
||||||
func (s *Store) List() ([]string, error) {
|
func (s *Store) List() ([]string, error) {
|
||||||
|
defer lock(&s.mu)()
|
||||||
|
|
||||||
|
if err := sanityCheck(s.basePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var relPaths []string
|
var relPaths []string
|
||||||
return relPaths, filepath.WalkDir(s.basePath, func(path string, d fs.DirEntry, err error) error {
|
return relPaths, filepath.WalkDir(s.basePath, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,30 +81,13 @@ func (s *Store) List() ([]string, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
attrPrefix = "#GONIC-"
|
|
||||||
attrName = "NAME"
|
|
||||||
attrCommment = "COMMENT"
|
|
||||||
attrIsPublic = "IS-PUBLIC"
|
|
||||||
)
|
|
||||||
|
|
||||||
func encodeAttr(name, value string) string {
|
|
||||||
return fmt.Sprintf("%s%s:%s", attrPrefix, name, strconv.Quote(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeAttr(line string) (name, value string) {
|
|
||||||
if !strings.HasPrefix(line, attrPrefix) {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
prefixAndName, rawValue, _ := strings.Cut(line, ":")
|
|
||||||
name = strings.TrimPrefix(prefixAndName, attrPrefix)
|
|
||||||
value, _ = strconv.Unquote(rawValue)
|
|
||||||
return name, value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Read(relPath string) (*Playlist, error) {
|
func (s *Store) Read(relPath string) (*Playlist, error) {
|
||||||
defer lock(&s.mu)()
|
defer lock(&s.mu)()
|
||||||
|
|
||||||
|
if err := sanityCheck(s.basePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
absPath := filepath.Join(s.basePath, relPath)
|
absPath := filepath.Join(s.basePath, relPath)
|
||||||
stat, err := os.Stat(absPath)
|
stat, err := os.Stat(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,6 +135,10 @@ func (s *Store) Read(relPath string) (*Playlist, error) {
|
|||||||
func (s *Store) Write(relPath string, playlist *Playlist) error {
|
func (s *Store) Write(relPath string, playlist *Playlist) error {
|
||||||
defer lock(&s.mu)()
|
defer lock(&s.mu)()
|
||||||
|
|
||||||
|
if err := sanityCheck(s.basePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
absPath := filepath.Join(s.basePath, relPath)
|
absPath := filepath.Join(s.basePath, relPath)
|
||||||
if err := os.MkdirAll(filepath.Dir(absPath), 0o777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(absPath), 0o777); err != nil {
|
||||||
return fmt.Errorf("make m3u base dir: %w", err)
|
return fmt.Errorf("make m3u base dir: %w", err)
|
||||||
@@ -217,6 +185,10 @@ func (s *Store) Write(relPath string, playlist *Playlist) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Delete(relPath string) error {
|
func (s *Store) Delete(relPath string) error {
|
||||||
|
if err := sanityCheck(s.basePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return os.Remove(filepath.Join(s.basePath, relPath))
|
return os.Remove(filepath.Join(s.basePath, relPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,3 +209,55 @@ func lock(mu *sync.Mutex) func() {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
return mu.Unlock
|
return mu.Unlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanityCheck(basePath string) error {
|
||||||
|
// sanity check layout, just in case someone tries to use an existing folder
|
||||||
|
entries, err := os.ReadDir(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sanity checking: reading dir: %w", err)
|
||||||
|
}
|
||||||
|
var found string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := userIDFromPath(entry.Name()); err != nil {
|
||||||
|
found = entry.Name()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found != "" {
|
||||||
|
return fmt.Errorf("sanity checking: %w: item %q in playlists directory is not a user id. see wiki for details on layout of the playlists dir", ErrNoUserPrefix, found)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
attrPrefix = "#GONIC-"
|
||||||
|
attrName = "NAME"
|
||||||
|
attrCommment = "COMMENT"
|
||||||
|
attrIsPublic = "IS-PUBLIC"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encodeAttr(name, value string) string {
|
||||||
|
return fmt.Sprintf("%s%s:%s", attrPrefix, name, strconv.Quote(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAttr(line string) (name, value string) {
|
||||||
|
if !strings.HasPrefix(line, attrPrefix) {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
prefixAndName, rawValue, _ := strings.Cut(line, ":")
|
||||||
|
name = strings.TrimPrefix(prefixAndName, attrPrefix)
|
||||||
|
value, _ = strconv.Unquote(rawValue)
|
||||||
|
return name, value
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPath(userID int, playlistName string) string {
|
||||||
|
playlistName = fileutil.Safe(playlistName)
|
||||||
|
if playlistName == "" {
|
||||||
|
playlistName = "pl"
|
||||||
|
}
|
||||||
|
playlistName = fmt.Sprintf("%s-%d%s", playlistName, time.Now().UnixMilli(), extM3U)
|
||||||
|
return filepath.Join(fmt.Sprint(userID), playlistName)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user