diff --git a/.gitignore b/.gitignore index 1c2d1939..9d3b79fb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ gonicscan gonicembed .vscode *.swp +.tags* diff --git a/README.md b/README.md index 7118ea17..60cbb7d0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - browsing by tags (using [taglib](https://taglib.org/) - supports mp3, opus, flac, ape, m4a, wav, etc.) - on-the-fly audio transcoding and caching (requires [ffmpeg](https://ffmpeg.org/)) (thank you [spijet](https://github.com/spijet/)) - jukebox mode (thank you [lxea](https://github.com/lxea/)) -- support for per-user podcasts (thank you [lxea](https://github.com/lxea/)) +- support for podcasts (thank you [lxea](https://github.com/lxea/)) - pretty fast scanning (with my library of ~27k tracks, initial scan takes about 10m, and about 5s after incrementally) - multiple users, each with their own transcoding preferences, playlists, top tracks, top artists, etc. - [last.fm](https://www.last.fm/) scrobbling diff --git a/db/migrations.go b/db/migrations.go index 6434bec6..a663320e 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -41,6 +41,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202202092013", migrateArtistCover), construct(ctx, "202202121809", migrateAlbumRootDirAgain), construct(ctx, "202202241218", migratePublicPlaylist), + construct(ctx, "202204270903", migratePodcastDropUserID), } return gormigrate. @@ -332,3 +333,26 @@ func migrateAlbumRootDirAgain(tx *gorm.DB, ctx MigrationContext) error { func migratePublicPlaylist(tx *gorm.DB, ctx MigrationContext) error { return tx.AutoMigrate(Playlist{}).Error } + +func migratePodcastDropUserID(tx *gorm.DB, _ MigrationContext) error { + step := tx.AutoMigrate( + Podcast{}, + ) + if err := step.Error; err != nil { + return fmt.Errorf("step auto migrate: %w", err) + } + + if !tx.Dialect().HasColumn("podcasts", "user_id") { + return nil + } + + + step = tx.Exec(` + ALTER TABLE podcasts DROP COLUMN user_id; + `) + if err := step.Error; err != nil { + return fmt.Errorf("step migrate podcasts drop user_id: %w", err) + } + return nil +} + diff --git a/db/model.go b/db/model.go index a97ba321..867b0066 100644 --- a/db/model.go +++ b/db/model.go @@ -7,15 +7,14 @@ package db import ( "path" - "path/filepath" "strconv" "strings" "time" // TODO: remove this dep - "go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/mime" + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) func splitInt(in, sep string) []int { @@ -315,7 +314,6 @@ type Podcast struct { ID int `gorm:"primary_key"` UpdatedAt time.Time ModifiedAt time.Time - UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` URL string Title string Description string @@ -326,11 +324,6 @@ type Podcast struct { AutoDownload PodcastAutoDownload } -func (p *Podcast) Fullpath(podcastPath string) string { - sanitizedTitle := strings.ReplaceAll(p.Title, "/", "_") - return filepath.Join(podcastPath, filepath.Clean(sanitizedTitle)) -} - func (p *Podcast) SID() *specid.ID { return &specid.ID{Type: specid.Podcast, Value: p.ID} } diff --git a/podcasts/podcasts.go b/podcasts/podcasts.go index 12b919a3..774a440d 100644 --- a/podcasts/podcasts.go +++ b/podcasts/podcasts.go @@ -40,13 +40,15 @@ func New(db *db.DB, base string, tagger tags.Reader) *Podcasts { } } -func (p *Podcasts) GetPodcastOrAll(userID int, id int, includeEpisodes bool) ([]*db.Podcast, error) { +func (p *Podcasts) GetPodcastOrAll(id int, includeEpisodes bool) ([]*db.Podcast, error) { + var err error podcasts := []*db.Podcast{} - q := p.db.Where("user_id=?", userID) if id != 0 { - q = q.Where("id=?", id) + err = p.db.Where("id=?", id).Find(&podcasts).Error + } else { + err = p.db.Find(&podcasts).Error } - if err := q.Find(&podcasts).Error; err != nil { + if err != nil { return nil, fmt.Errorf("finding podcasts: %w", err) } if !includeEpisodes { @@ -88,16 +90,14 @@ func (p *Podcasts) GetNewestPodcastEpisodes(count int) ([]*db.PodcastEpisode, er return episodes, nil } -func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed, - userID int) (*db.Podcast, error) { +func (p *Podcasts) AddNewPodcast(rssURL string, feed *gofeed.Feed) (*db.Podcast, error) { podcast := db.Podcast{ Description: feed.Description, ImageURL: feed.Image.URL, - UserID: userID, Title: feed.Title, URL: rssURL, } - podPath := podcast.Fullpath(p.baseDir) + podPath := absPath(p.baseDir, &podcast) err := os.Mkdir(podPath, 0755) if err != nil && !os.IsExist(err) { return nil, err @@ -252,8 +252,7 @@ func itemToEpisode(podcastID, size, duration int, audio string, } } -func (p *Podcasts) findEnclosureAudio(podcastID, duration int, - item *gofeed.Item) (*db.PodcastEpisode, bool) { +func (p *Podcasts) findEnclosureAudio(podcastID, duration int, item *gofeed.Item) (*db.PodcastEpisode, bool) { for _, enc := range item.Enclosures { if !isAudio(enc.Type, enc.URL) { continue @@ -264,8 +263,7 @@ func (p *Podcasts) findEnclosureAudio(podcastID, duration int, return nil, false } -func (p *Podcasts) findMediaAudio(podcastID, duration int, - item *gofeed.Item) (*db.PodcastEpisode, bool) { +func (p *Podcasts) findMediaAudio(podcastID, duration int, item *gofeed.Item) (*db.PodcastEpisode, bool) { extensions, ok := item.Extensions["media"]["content"] if !ok { return nil, false @@ -291,22 +289,6 @@ func (p *Podcasts) RefreshPodcasts() error { return nil } -func (p *Podcasts) RefreshPodcastsForUser(userID int) error { - podcasts := []*db.Podcast{} - err := p.db. - Where("user_id=?", userID). - Find(&podcasts). - Error - if err != nil { - return fmt.Errorf("find podcasts: %w", err) - } - var errs *multierr.Err - if errors.As(p.refreshPodcasts(podcasts), &errs) && errs.Len() > 0 { - return fmt.Errorf("refresh podcasts: %w", errs) - } - return nil -} - func (p *Podcasts) refreshPodcasts(podcasts []*db.Podcast) error { errs := &multierr.Err{} for _, podcast := range podcasts { @@ -387,13 +369,12 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error { filename = path.Base(audioURL.Path) } filename = p.findUniqueEpisodeName(&podcast, &podcastEpisode, filename) - audioFile, err := os.Create(path.Join(podcast.Fullpath(p.baseDir), filename)) + audioFile, err := os.Create(path.Join(absPath(p.baseDir, &podcast), filename)) if err != nil { return fmt.Errorf("create audio file: %w", err) } podcastEpisode.Filename = filename - sanTitle := strings.ReplaceAll(podcast.Title, "/", "_") - podcastEpisode.Path = path.Join(sanTitle, filename) + podcastEpisode.Path = path.Join(pathSafe(podcast.Title), filename) p.db.Save(&podcastEpisode) go func() { if err := p.doPodcastDownload(&podcastEpisode, audioFile, resp.Body); err != nil { @@ -403,22 +384,18 @@ func (p *Podcasts) DownloadEpisode(episodeID int) error { return nil } -func (p *Podcasts) findUniqueEpisodeName( - podcast *db.Podcast, - podcastEpisode *db.PodcastEpisode, - filename string) string { - podcastPath := path.Join(podcast.Fullpath(p.baseDir), filename) +func (p *Podcasts) findUniqueEpisodeName(podcast *db.Podcast, podcastEpisode *db.PodcastEpisode, filename string) string { + podcastPath := path.Join(absPath(p.baseDir, podcast), filename) if _, err := os.Stat(podcastPath); os.IsNotExist(err) { return filename } - sanitizedTitle := strings.ReplaceAll(podcastEpisode.Title, "/", "_") - titlePath := fmt.Sprintf("%s%s", sanitizedTitle, filepath.Ext(filename)) - podcastPath = path.Join(podcast.Fullpath(p.baseDir), titlePath) + titlePath := fmt.Sprintf("%s%s", pathSafe(podcastEpisode.Title), filepath.Ext(filename)) + podcastPath = path.Join(absPath(p.baseDir, podcast), titlePath) if _, err := os.Stat(podcastPath); os.IsNotExist(err) { return titlePath } // try to find a filename like FILENAME (1).mp3 incrementing - return findEpisode(podcast.Fullpath(p.baseDir), filename, 1) + return findEpisode(absPath(p.baseDir, podcast), filename, 1) } func findEpisode(base, filename string, count int) string { @@ -462,9 +439,7 @@ func (p *Podcasts) downloadPodcastCover(podPath string, podcast *db.Podcast) err if _, err := io.Copy(coverFile, resp.Body); err != nil { return fmt.Errorf("writing podcast cover: %w", err) } - podcastPath := filepath.Clean(strings.ReplaceAll(podcast.Title, "/", "_")) - podcastFilename := fmt.Sprintf("cover%s", ext) - podcast.ImagePath = path.Join(podcastPath, podcastFilename) + podcast.ImagePath = path.Join(pathSafe(podcast.Title), fmt.Sprintf("cover%s", ext)) if err := p.db.Save(podcast).Error; err != nil { return fmt.Errorf("save podcast: %w", err) } @@ -492,28 +467,20 @@ func (p *Podcasts) doPodcastDownload(podcastEpisode *db.PodcastEpisode, file *os return p.db.Save(podcastEpisode).Error } -func (p *Podcasts) DeletePodcast(userID, podcastID int) error { +func (p *Podcasts) DeletePodcast(podcastID int) error { podcast := db.Podcast{} err := p.db. - Where("id=? AND user_id=?", podcastID, userID). + Where("id=?", podcastID). First(&podcast). Error if err != nil { return err } - var userCount int - p.db. - Model(&db.Podcast{}). - Where("title=?", podcast.Title). - Count(&userCount) - if userCount == 1 { - // only delete the folder if there are not multiple listeners - if err = os.RemoveAll(podcast.Fullpath(p.baseDir)); err != nil { - return fmt.Errorf("delete podcast directory: %w", err) - } + if err := os.RemoveAll(absPath(p.baseDir, &podcast)); err != nil { + return fmt.Errorf("delete podcast directory: %w", err) } err = p.db. - Where("id=? AND user_id=?", podcastID, userID). + Where("id=?", podcastID). Delete(db.Podcast{}). Error if err != nil { @@ -535,3 +502,11 @@ func (p *Podcasts) DeletePodcastEpisode(podcastEpisodeID int) error { } return err } + +func pathSafe(in string) string { + return filepath.Clean(strings.ReplaceAll(in, string(filepath.Separator), "_")) +} + +func absPath(base string, p *db.Podcast) string { + return filepath.Join(base, pathSafe(p.Title)) +} diff --git a/server/assets/pages/home.tmpl b/server/assets/pages/home.tmpl index aa7727ef..da2484cf 100644 --- a/server/assets/pages/home.tmpl +++ b/server/assets/pages/home.tmpl @@ -169,43 +169,45 @@ -
you can add podcasts rss feeds here
-{{ $pref.Title }} | ++ | + | + | + | ||
{{ $pref.Title }} | -- | - | - | + + | + | |
- | - |