Merge remote-tracking branch 'upstream/master' into path-format

pull/108/head
Arno Hautala 2 years ago
commit c8f6e73f59

@ -14,7 +14,7 @@
</a> -->
<h1 align="center" style="margin-bottom:0px">Podgrab</h1>
<p align="center">Current Version -2021.08.25</p>
<p align="center">Current Version -2022.07.07</p>
<p align="center">
A self-hosted podcast manager to download episodes as soon as they become live

@ -1,4 +1,6 @@
{{define "commoncss"}}
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link
rel="stylesheet"
href="/webassets/skeleton.min.css"

@ -67,7 +67,7 @@
class="u-full-width"
src="/podcastitems/{{.ID}}/image"
alt="{{ .Title }}"
lazy="lazy"
loading="lazy"
/>
</div>
<div class="columns ten">

@ -125,7 +125,7 @@
class="u-full-width"
:src=" getEpisodeImage(item)"
v-bind:alt="item.Title"
lazy="lazy"
loading="lazy"
/>
</div>
<div class="columns ten">

@ -74,6 +74,7 @@
border:1px solid;
padding: 10px;
width: 300px;
z-index: 1000;
}
.tag-editor a.pill{
@ -327,7 +328,7 @@
<td>${ detailPodcast.Author }</td>
</tr>
<tr>
<td>Url</td>
<td>Original Url</td>
<td> <a target="_blank" :href="detailPodcast.URL">Link</a></td>
</tr>
@ -347,6 +348,10 @@
<td>Paused</td>
<td> ${ detailPodcast.IsPaused?'Yes':'No' }</td>
</tr>
<tr>
<td>Podgrab Feed</td>
<td> <a target="_blank" :href="'/podcasts/'+detailPodcast.ID+'/rss'">Link</a></td>
</tr>
</table>
</Modal>

@ -694,7 +694,7 @@ div#large-visualization{
<div id="player-left-bottom">
<div id="time-container">
<span class="current-time">
<span class="amplitude-current-minutes" ></span>:<span class="amplitude-current-seconds"></span>
<span class="amplitude-current-hours" ></span>:<span class="amplitude-current-minutes" ></span>:<span class="amplitude-current-seconds"></span>
</span>
<div id="progress-container">
@ -953,21 +953,12 @@ div#large-visualization{
"autoplay": true,
"volume":volume,
"callbacks": {
// 'song_change':function(){
// song = Amplitude.getActiveSongMetadata();
// if(self.songLoaded.indexOf(song.id)!==-1){
// return;
// }
// time= self.getSavedSongTime();
// console.log('change',time)
// if(time>0){
// self.songLoaded.push(song.id);
// Amplitude.skipTo(time,song.index);
// setTimeout(() => {
// self.songLoaded=self.songLoaded.splice(self.songLoaded.indexOf(song.id),1)
// }, 500);
// }
// },
'song_change':function(){
if(localStorage && localStorage.playerVolume){
volume=parseInt(localStorage.playerVolume)
Amplitude.setVolume(volume);
}
},
'timeupdate':function(){
var secs=Math.floor(Amplitude.getSongPlayedSeconds());

@ -49,8 +49,11 @@ function insertText(elemID, text){
<div class="columns two">
<a href="/backups" class="button">Backups</a>
</div>
<div class="columns two">
<a href="/opml" class="button">Export OPML</a>
<div class="columns three">
<a href="/opml" class="button" title="Export OPML file with original podcast urls">Export OPML (Original Urls)</a>
</div>
<div class="columns three">
<a href="/opml?usePodgrabLink=true" class="button" title="Export OPML file with Podgrab podcast feed urls">Export OPML (Podgrab Urls)</a>
</div>
<div class="columns two">
<a title="Import this rss feed in your favorite podcast player" target="_blank" href="/rss" class="button">Rss Feed</a>
@ -129,7 +132,14 @@ function insertText(elemID, text){
<span class="label-body">Base URL (if accessing Podgrab using a URL. Without trailing /. Leave empty if not using or unsure.)</span>
<input type="url" class="u-full-width" name="baseUrl" v-model="baseUrl">
</label>
<label for="maxDownloadConcurrency" style="display: inline-block;" >
<span class="label-body">Limit the number of podcasts that can be downloaded simultaneously</span>
<input type="number" name="maxDownloadConcurrency" v-model.number="maxDownloadConcurrency" min="1">
</label>
<label for="userAgent" style="display: inline-block;" >
<span class="label-body">The <code>User-Agent</code> header used when downloading podcasts</span>
<input type="text" class="u-full-width" name="userAgent" v-model="userAgent">
</label>
<input type="submit" value="Save" class="button">
</form>
@ -160,7 +170,7 @@ function insertText(elemID, text){
<table>
<tr>
<td>Current Version</td>
<td> 2021.08.25</td>
<td> 2022.07.07</td>
</tr>
<tr>
<td>Website</td>
@ -206,6 +216,8 @@ var app = new Vue({
generateNFOFile:self.generateNFOFile,
dontDownloadDeletedFromDisk:self.dontDownloadDeletedFromDisk,
baseUrl:self.baseUrl,
maxDownloadConcurrency:self.maxDownloadConcurrency,
userAgent:self.userAgent,
})
.then(function(response){
Vue.toasted.show('Settings saved successfully.' ,{
@ -249,6 +261,8 @@ var app = new Vue({
generateNFOFile:{{ .setting.GenerateNFOFile }},
dontDownloadDeletedFromDisk:{{ .setting.DontDownloadDeletedFromDisk }},
baseUrl: {{ .setting.BaseUrl }},
maxDownloadConcurrency:{{ .setting.MaxDownloadConcurrency }},
userAgent:{{ .setting.UserAgent}},
},
})

@ -30,6 +30,8 @@ type SettingModel struct {
GenerateNFOFile bool `form:"generateNFOFile" json:"generateNFOFile" query:"generateNFOFile"`
DontDownloadDeletedFromDisk bool `form:"dontDownloadDeletedFromDisk" json:"dontDownloadDeletedFromDisk" query:"dontDownloadDeletedFromDisk"`
BaseUrl string `form:"baseUrl" json:"baseUrl" query:"baseUrl"`
MaxDownloadConcurrency int `form:"maxDownloadConcurrency" json:"maxDownloadConcurrency" query:"maxDownloadConcurrency"`
UserAgent string `form:"userAgent" json:"userAgent" query:"userAgent"`
}
var searchOptions = map[string]string{
@ -331,7 +333,10 @@ func Search(c *gin.Context) {
}
func GetOmpl(c *gin.Context) {
data, err := service.ExportOmpl()
usePodgrabLink := c.DefaultQuery("usePodgrabLink", "false") == "true"
data, err := service.ExportOmpl(usePodgrabLink, getBaseUrl(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request"})
return

@ -289,7 +289,7 @@ func GetPodcastItemFileById(c *gin.Context) {
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", "attachment; filename="+path.Base(podcast.DownloadPath))
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Type", GetFileContentType(podcast.DownloadPath))
c.File(podcast.DownloadPath)
} else {
c.Redirect(302, podcast.FileURL)
@ -300,6 +300,19 @@ func GetPodcastItemFileById(c *gin.Context) {
}
}
func GetFileContentType(filePath string) string {
file, err := os.Open(filePath)
if err != nil {
return "application/octet-stream"
}
defer file.Close()
buffer := make([]byte, 512)
if _, err := file.Read(buffer); err != nil {
return "application/octet-stream"
}
return http.DetectContentType(buffer)
}
func MarkPodcastItemAsUnplayed(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
@ -439,7 +452,7 @@ func getBaseUrl(c *gin.Context) string {
return setting.BaseUrl
}
func createRss(items []db.PodcastItem, title, description string, c *gin.Context) model.RssPodcastData {
func createRss(items []db.PodcastItem, title, description, image string, c *gin.Context) model.RssPodcastData {
var rssItems []model.RssItem
url := getBaseUrl(c)
for _, item := range items {
@ -468,6 +481,12 @@ func createRss(items []db.PodcastItem, title, description string, c *gin.Context
}
rssItems = append(rssItems, rssItem)
}
imagePath := fmt.Sprintf("%s/webassets/blank.png", url)
if image != "" {
imagePath = image
}
return model.RssPodcastData{
Itunes: "http://www.itunes.com/dtds/podcast-1.0.dtd",
Media: "http://search.yahoo.com/mrss/",
@ -482,11 +501,33 @@ func createRss(items []db.PodcastItem, title, description string, c *gin.Context
Summary: description,
Author: "Podgrab Aggregation",
Link: fmt.Sprintf("%s/allTags", url),
Image: model.RssItemImage{Text: title, Href: fmt.Sprintf("%s/webassets/blank.png", url)},
Image: model.RssItemImage{Text: title, URL: imagePath},
},
}
}
func GetRssForPodcastById(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
var podcast db.Podcast
err := db.GetPodcastById(searchByIdQuery.Id, &podcast)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
var podIds []string
podIds = append(podIds, searchByIdQuery.Id)
items := *service.GetAllPodcastItemsByPodcastIds(podIds)
description := podcast.Summary
title := podcast.Title
if err == nil {
c.XML(200, createRss(items, title, description, podcast.Image, c))
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func GetRssForTagById(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
@ -501,7 +542,7 @@ func GetRssForTagById(c *gin.Context) {
title := fmt.Sprintf(" %s | Podgrab", tag.Label)
if err == nil {
c.XML(200, createRss(items, title, description, c))
c.XML(200, createRss(items, title, description, "", c))
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
@ -518,7 +559,7 @@ func GetRss(c *gin.Context) {
title := "Podgrab"
description := "Pograb playlist"
c.XML(200, createRss(items, title, description, c))
c.XML(200, createRss(items, title, description, "", c))
}
func DeleteTagById(c *gin.Context) {
@ -586,15 +627,18 @@ func UpdateSetting(c *gin.Context) {
if err == nil {
err = service.UpdateSettings(
model.DownloadOnAdd,
model.InitialDownloadCount,
model.DownloadOnAdd,
model.InitialDownloadCount,
model.AutoDownload,
model.FileNameFormat,
model.DarkMode,
model.DownloadEpisodeImages,
model.GenerateNFOFile,
model.DontDownloadDeletedFromDisk,
model.BaseUrl)
model.DownloadEpisodeImages,
model.GenerateNFOFile,
model.DontDownloadDeletedFromDisk,
model.BaseUrl,
model.MaxDownloadConcurrency
model.UserAgent,
)
if err == nil {
c.JSON(200, gin.H{"message": "Success"})

@ -86,6 +86,8 @@ type Setting struct {
GenerateNFOFile bool `gorm:"default:false"`
DontDownloadDeletedFromDisk bool `gorm:"default:false"`
BaseUrl string
MaxDownloadConcurrency int `gorm:"default:5"`
UserAgent string
}
type Migration struct {
Base

@ -159,6 +159,7 @@ func main() {
router.DELETE("/podcasts/:id/podcast", controllers.DeleteOnlyPodcastById)
router.GET("/podcasts/:id/pause", controllers.PausePodcastById)
router.GET("/podcasts/:id/unpause", controllers.UnpausePodcastById)
router.GET("/podcasts/:id/rss", controllers.GetRssForPodcastById)
router.GET("/podcastitems", controllers.GetAllPodcastItems)
router.GET("/podcastitems/:id", controllers.GetPodcastItemById)

@ -56,6 +56,7 @@ type RssItemEnclosure struct {
type RssItemImage struct {
Text string `xml:",chardata"`
Href string `xml:"href,attr"`
URL string `xml:"url"`
}
type RssItemGuid struct {

@ -27,7 +27,13 @@ func Download(link string, episodeTitle string, podcastName string, episodePathN
return "", errors.New("Download path empty")
}
client := httpClient()
resp, err := client.Get(link)
req, err := getRequest(link)
if err != nil {
Logger.Errorw("Error creating request: "+link, err)
}
resp, err := client.Do(req)
if err != nil {
Logger.Errorw("Error getting response: "+link, err)
return "", err
@ -103,7 +109,13 @@ func DownloadPodcastCoverImage(link string, podcastName string) (string, error)
return "", errors.New("Download path empty")
}
client := httpClient()
resp, err := client.Get(link)
req, err := getRequest(link)
if err != nil {
Logger.Errorw("Error creating request: "+link, err)
return "", err
}
resp, err := client.Do(req)
if err != nil {
Logger.Errorw("Error getting response: "+link, err)
return "", err
@ -140,7 +152,13 @@ func DownloadImage(link string, episodeId string, podcastName string) (string, e
return "", errors.New("Download path empty")
}
client := httpClient()
resp, err := client.Get(link)
req, err := getRequest(link)
if err != nil {
Logger.Errorw("Error creating request: "+link, err)
return "", err
}
resp, err := client.Do(req)
if err != nil {
Logger.Errorw("Error getting response: "+link, err)
return "", err
@ -327,6 +345,20 @@ func httpClient() *http.Client {
return &client
}
func getRequest(url string) (*http.Request, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
setting := db.GetOrCreateSetting()
if len(setting.UserAgent) > 0 {
req.Header.Add("User-Agent", setting.UserAgent)
}
return req, nil
}
func createPreSanitizedPath(folderPath string) string {
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
os.MkdirAll(folderPath, 0777)
@ -351,6 +383,10 @@ func createConfigFolderIfNotExists(folder string) string {
return createFolder(folder, dataPath)
}
func deletePodcastFolder(folder string) error {
return os.RemoveAll(createDataFolderIfNotExists(folder))
}
func getFileName(link string, title string, defaultExtension string) string {
fileUrl, err := url.Parse(link)
checkError(err)

@ -143,14 +143,22 @@ func AddOpml(content string) error {
}
func ExportOmpl() ([]byte, error) {
func ExportOmpl(usePodgrabLink bool, baseUrl string) ([]byte, error) {
podcasts := GetAllPodcasts("")
var outlines []model.OpmlOutline
for _, podcast := range *podcasts {
xmlUrl := podcast.URL
if usePodgrabLink {
xmlUrl = fmt.Sprintf("%s/podcasts/%s/rss", baseUrl, podcast.ID)
}
toAdd := model.OpmlOutline{
AttrText: podcast.Summary,
Type: "rss",
XmlUrl: podcast.URL,
XmlUrl: xmlUrl,
Title: podcast.Title,
}
outlines = append(outlines, toAdd)
@ -578,7 +586,7 @@ func DownloadMissingEpisodes() error {
SetPodcastItemAsDownloaded(item.ID, url)
}(item, *setting)
if index%5 == 0 {
if index%setting.MaxDownloadConcurrency == 0 {
wg.Wait()
}
}
@ -726,6 +734,12 @@ func DeletePodcast(id string, deleteFiles bool) error {
db.DeletePodcastItemById(item.ID)
}
err = deletePodcastFolder(podcast.Title)
if err != nil {
return err
}
err = db.DeletePodcastById(id)
if err != nil {
return err
@ -798,7 +812,19 @@ func GetSearchFromPodcastIndex(pod *podcastindex.Podcast) *model.CommonSearchRes
return p
}
func UpdateSettings(downloadOnAdd bool, initialDownloadCount int, autoDownload bool, fileNameFormat string, darkMode bool, downloadEpisodeImages bool, generateNFOFile bool, dontDownloadDeletedFromDisk bool, baseUrl string) error {
func UpdateSettings(
downloadOnAdd bool,
initialDownloadCount int,
autoDownload bool,
fileNameFormat string,
darkMode bool,
downloadEpisodeImages bool,
generateNFOFile bool,
dontDownloadDeletedFromDisk bool,
baseUrl string,
maxDownloadConcurrency int,
userAgent string,
) error {
setting := db.GetOrCreateSetting()
setting.AutoDownload = autoDownload
@ -810,6 +836,8 @@ func UpdateSettings(downloadOnAdd bool, initialDownloadCount int, autoDownload b
setting.GenerateNFOFile = generateNFOFile
setting.DontDownloadDeletedFromDisk = dontDownloadDeletedFromDisk
setting.BaseUrl = baseUrl
setting.MaxDownloadConcurrency = maxDownloadConcurrency
setting.UserAgent = userAgent
return db.UpdateSettings(setting)
}

Loading…
Cancel
Save