tags complete

swagger
Akhil Gupta 4 years ago
parent 3315758a17
commit a161bb2aa9

@ -0,0 +1,7 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

@ -71,9 +71,10 @@ Podgrab started a tool that I initially built to solve a specific problem I had.
### Features ### Features
- Download/Archive complete podcast - Download/Archive complete podcast
- Auto-download new episodes - Auto-download new episodes
- Tag/Label podcasts into groups
- Download on demand - Download on demand
- Podcast Discovery - Search and Add podcasts using iTunes API - Podcast Discovery - Search and Add podcasts using iTunes API
- Full-fledged podcast player - Play downloaded files or stream from original source. - Full-fledged podcast player - Play downloaded files or stream from original source. Play single episodes, full podcasts and podcast groups(tags)
- Add using direct RSS feed URL / OMPL import / Search - Add using direct RSS feed URL / OMPL import / Search
- Basic Authentication - Basic Authentication
- Existing episode file detection - Prevent re-downloading files if already present - Existing episode file detection - Prevent re-downloading files if already present

@ -121,7 +121,7 @@
</select> </select>
<select v-model="filterTag" name="" id=""> <select v-model="filterTag" v-if="allTags.length">
<option value="">All</option> <option value="">All</option>
<option v-for="option in allTags" v-bind:value="option.ID"> <option v-for="option in allTags" v-bind:value="option.ID">
${option.Label} (${option.Podcasts.length}) ${option.Label} (${option.Podcasts.length})

@ -175,8 +175,8 @@ onclick="enquePodcast({{ .podcastId }})"
<li class="navbar-item"><a class="navbar-link" href="/episodes">Episodes</a></li> <li class="navbar-item"><a class="navbar-link" href="/episodes">Episodes</a></li>
<li class="navbar-item"><a class="navbar-link" href="/add">Add Podcast</a></li> <li class="navbar-item"><a class="navbar-link" href="/add">Add Podcast</a></li>
<li class="navbar-item"><a class="navbar-link" href="/player">Player</a></li> <li class="navbar-item"><a class="navbar-link" href="/player">Player</a></li>
<li class="navbar-item"><a class="navbar-link" href="/allTags">Tags</a></li>
<li class="navbar-item"><a class="navbar-link" href="/settings">Settings</a></li> <li class="navbar-item"><a class="navbar-link" href="/settings">Settings</a></li>
</ul> </ul>
</div> </div>
</nav> </nav>
@ -187,6 +187,7 @@ onclick="enquePodcast({{ .podcastId }})"
<li><a href="/episodes">Episodes</a></li> <li><a href="/episodes">Episodes</a></li>
<li><a href="/add">Add Podcast</a></li> <li><a href="/add">Add Podcast</a></li>
<li><a href="/player">Player</a></li> <li><a href="/player">Player</a></li>
<li><a href="/allTags">Tags</a></li>
<li><a href="/settings">Settings</a></li> <li><a href="/settings">Settings</a></li>
</ul> </ul>
<div id="closeDrawer" onclick="toggleMenu()" > <div id="closeDrawer" onclick="toggleMenu()" >

@ -79,7 +79,7 @@
} }
checkUseMore(); checkUseMore();
function openPlayer(itemId, podcastId) { function openPlayer(itemId, podcastId,tagIds) {
var url = "/player?"; var url = "/player?";
if (itemId) { if (itemId) {
url += "&itemId=" + itemId; url += "&itemId=" + itemId;
@ -87,6 +87,11 @@
if (podcastId) { if (podcastId) {
url += "&podcastId=" + podcastId; url += "&podcastId=" + podcastId;
} }
if(tagIds && tagIds.length>0){
for (const tagId of tagIds) {
url += "&tagIds=" + tagId;
}
}
const player = window.open(url, "podgrab_player"); const player = window.open(url, "podgrab_player");
} }

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.title}} - PodGrab</title>
{{template "commoncss" .}}
<style>
img {
display: none;
}
h2,
h3,
h4,
h5 {
margin-bottom: 1rem;
}
h4 {
font-size: 2rem;
}
h5 {
font-size: 1.5rem;
}
p {
margin-bottom: 0.5rem;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
}
.tag .button {
padding: 0 15px;
}
.IsPlayed-true {
color: #555555;
}
/* Larger than tablet */
@media (min-width: 750px) {
img {
display: block;
}
}
.button-enqueue {
display: none;
}
body.playerExists .button-enqueue {
display: inline-block;
}
.podcast-image {
width: 100px;
display: inline-block;
}
</style>
</head>
<body>
<div class="container">
{{template "navbar" .}}
<br />
<br />{{$setting := .setting}} {{range .tags}}
<div id="tag-{{.ID}}">
<div class="podcasts row tag">
<div class="columns two">
<h4>{{.Label}}</h4>
</div>
<div class="columns seven">
{{range .Podcasts}}
<img
class="podcast-image"
src="{{.Image}}"
title="{{.Title}}"
alt="{{.Title}}"
srcset=""
/>
{{end}}
</div>
<div class="columns three">
<a
class="button button"
onclick="deleteTag('{{.ID}}')"
title="Delete Tag"
><i class="fas fa-trash"></i
></a>
<a
class="button button"
onclick="playTag('{{.ID}}')"
title="Play all episodes of this tag"
><i class="fas fa-play"></i
></a>
<a
class="button button-enqueue"
onclick="enqueueTag('{{.ID}}')"
title="Add Episode to existing player playlist"
><i class="fas fa-plus"></i
></a>
</div>
</div>
<hr />
</div>
{{else}}
<div class="welcome">
<p>
It seems you haven't created any tags yet. Try creating a few new tags
on the podcast listing page.
</p>
</div>
{{end}}
<div class="row">
<div class="columns twelve" style="text-align: center">
{{if .previousPage }}
<a
href="?page={{.previousPage}}&downloadedOnly={{.downloadedOnly}}"
class="button"
>Newer</a
>
{{end}} {{if .nextPage }}
<a
href="?page={{.nextPage}}&downloadedOnly={{.downloadedOnly}}"
class="button"
>Older</a
>
{{end}}
</div>
</div>
</div>
{{template "scripts"}}
<script>
function deleteTag(id) {
var confirmed = confirm(
"Are you sure you want to delete this tag? This action cannot be reversed."
);
if (!confirmed) {
return false;
}
axios
.delete("/tags/" + id )
.then(function (response) {
Vue.toasted.show("Tag deleted.", {
theme: "bubble",
type: "success",
position: "top-right",
duration: 5000,
});
var row = document.getElementById("tag-" + id);
row.remove();
})
.catch(function (error) {
if (
error.response &&
error.response.data &&
error.response.data.message
) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
return false;
}
</script>
<script>
const socket = getWebsocketConnection(
function (event) {
const message = getWebsocketMessage("Register", "Tags");
socket.send(message);
},
function (x) {
const msg = JSON.parse(x.data);
if (msg.messageType == "NoPlayer") {
document.body.classList.remove("playerExists");
}
if (msg.messageType == "PlayerExists") {
document.body.classList.add("playerExists");
}
}
);
function enqueueTag(id) {
if (!socket) {
return;
}
socket.send(getWebsocketMessage("Enqueue", `{"tagIds":["${id}"]}`));
Vue.toasted.show("Episodes enqueued.", {
theme: "bubble",
type: "success",
position: "top-right",
duration: 5000,
});
}
function playTag(id) {
openPlayer("","", [id]);
}
</script>
</body>
</html>

@ -96,7 +96,7 @@ func PodcastPage(c *gin.Context) {
} }
func getItemsToPlay(itemId, podcastId string) []db.PodcastItem { func getItemsToPlay(itemId, podcastId string, tagIds []string) []db.PodcastItem {
var items []db.PodcastItem var items []db.PodcastItem
if itemId != "" { if itemId != "" {
toAdd := service.GetPodcastItemById(itemId) toAdd := service.GetPodcastItemById(itemId)
@ -104,12 +104,18 @@ func getItemsToPlay(itemId, podcastId string) []db.PodcastItem {
} else if podcastId != "" { } else if podcastId != "" {
pod := service.GetPodcastById(podcastId) pod := service.GetPodcastById(podcastId)
// for _, item := range pod.PodcastItems {
// if item.DownloadStatus == db.Downloaded {
// items = append(items, item)
// }
// }
items = pod.PodcastItems items = pod.PodcastItems
} else if len(tagIds) != 0 {
tags := service.GetTagsByIds(tagIds)
var tagNames []string
var podIds []string
for _, tag := range *tags {
tagNames = append(tagNames, tag.Label)
for _, pod := range tag.Podcasts {
podIds = append(podIds, pod.ID)
}
}
items = *service.GetAllPodcastItemsByPodcastIds(podIds)
} }
return items return items
} }
@ -118,6 +124,7 @@ func PlayerPage(c *gin.Context) {
itemId, hasItemId := c.GetQuery("itemId") itemId, hasItemId := c.GetQuery("itemId")
podcastId, hasPodcastId := c.GetQuery("podcastId") podcastId, hasPodcastId := c.GetQuery("podcastId")
tagIds, hasTagIds := c.GetQueryArray("tagIds")
title := "Podgrab" title := "Podgrab"
var items []db.PodcastItem var items []db.PodcastItem
var totalCount int64 var totalCount int64
@ -127,14 +134,25 @@ func PlayerPage(c *gin.Context) {
totalCount = 1 totalCount = 1
} else if hasPodcastId { } else if hasPodcastId {
pod := service.GetPodcastById(podcastId) pod := service.GetPodcastById(podcastId)
// for _, item := range pod.PodcastItems {
// if item.DownloadStatus == db.Downloaded {
// items = append(items, item)
// }
// }
items = pod.PodcastItems items = pod.PodcastItems
title = "Playing: " + pod.Title title = "Playing: " + pod.Title
totalCount = int64(len(items)) totalCount = int64(len(items))
} else if hasTagIds {
tags := service.GetTagsByIds(tagIds)
var tagNames []string
var podIds []string
for _, tag := range *tags {
tagNames = append(tagNames, tag.Label)
for _, pod := range tag.Podcasts {
podIds = append(podIds, pod.ID)
}
}
items = *service.GetAllPodcastItemsByPodcastIds(podIds)
if len(tagNames) == 1 {
title = fmt.Sprintf("Playing episodes with tag : %s", (tagNames[0]))
} else {
title = fmt.Sprintf("Playing episodes with tags : %s", strings.Join(tagNames, ", "))
}
} else { } else {
title = "Playing Latest Episodes" title = "Playing Latest Episodes"
if err := db.GetPaginatedPodcastItems(1, 20, nil, nil, time.Time{}, &items, &totalCount); err != nil { if err := db.GetPaginatedPodcastItems(1, 20, nil, nil, time.Time{}, &items, &totalCount); err != nil {
@ -255,6 +273,51 @@ func AllEpisodesPage(c *gin.Context) {
} }
func AllTagsPage(c *gin.Context) {
var pagination Pagination
var page, count int
c.ShouldBindQuery(&pagination)
if page = pagination.Page; page == 0 {
page = 1
}
if count = pagination.Count; count == 0 {
count = 10
}
var tags []db.Tag
var totalCount int64
//fmt.Printf("%+v\n", filter)
if err := db.GetPaginatedTags(page, count,
&tags, &totalCount); err == nil {
setting := c.MustGet("setting").(*db.Setting)
totalPages := math.Ceil(float64(totalCount) / float64(count))
nextPage, previousPage := 0, 0
if float64(page) < totalPages {
nextPage = page + 1
}
if page > 1 {
previousPage = page - 1
}
toReturn := gin.H{
"title": "Tags",
"tags": tags,
"setting": setting,
"page": page,
"count": count,
"totalCount": totalCount,
"totalPages": totalPages,
"nextPage": nextPage,
"previousPage": previousPage,
}
c.HTML(http.StatusOK, "tags.html", toReturn)
} else {
c.JSON(http.StatusBadRequest, err)
}
}
func Search(c *gin.Context) { func Search(c *gin.Context) {
var searchQuery SearchGPodderData var searchQuery SearchGPodderData
if c.ShouldBindQuery(&searchQuery) == nil { if c.ShouldBindQuery(&searchQuery) == nil {

@ -350,6 +350,17 @@ func GetTagById(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
} }
} }
func DeleteTagById(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
err := service.DeleteTag(searchByIdQuery.Id)
if err == nil {
c.JSON(http.StatusNoContent, gin.H{})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func AddTag(c *gin.Context) { func AddTag(c *gin.Context) {
var addTagData AddTagData var addTagData AddTagData
err := c.ShouldBindJSON(&addTagData) err := c.ShouldBindJSON(&addTagData)

@ -9,8 +9,9 @@ import (
) )
type EnqueuePayload struct { type EnqueuePayload struct {
ItemId string `json:"itemId"` ItemId string `json:"itemId"`
PodcastId string `json:"podcastId"` PodcastId string `json:"podcastId"`
TagIds []string `json:"tagIds"`
} }
var wsupgrader = websocket.Upgrader{ var wsupgrader = websocket.Upgrader{
@ -90,7 +91,7 @@ func HandleWebsocketMessages() {
fmt.Println(msg.Payload) fmt.Println(msg.Payload)
err := json.Unmarshal([]byte(msg.Payload), &payload) err := json.Unmarshal([]byte(msg.Payload), &payload)
if err == nil { if err == nil {
items := getItemsToPlay(payload.ItemId, payload.PodcastId) items := getItemsToPlay(payload.ItemId, payload.PodcastId, payload.TagIds)
var player *websocket.Conn var player *websocket.Conn
for connection, id := range activePlayers { for connection, id := range activePlayers {

@ -57,6 +57,15 @@ func GetPaginatedPodcastItems(page int, count int, downloadedOnly *bool, playedO
return result.Error return result.Error
} }
func GetPaginatedTags(page int, count int, tags *[]Tag, total *int64) error {
query := DB.Preload("Podcasts")
result := query.Limit(count).Offset((page - 1) * count).Order("created_at desc").Find(&tags)
query.Count(total)
return result.Error
}
func GetPodcastById(id string, podcast *Podcast) error { func GetPodcastById(id string, podcast *Podcast) error {
result := DB.Preload("PodcastItems", func(db *gorm.DB) *gorm.DB { result := DB.Preload("PodcastItems", func(db *gorm.DB) *gorm.DB {
@ -81,11 +90,22 @@ func DeletePodcastById(id string) error {
return result.Error return result.Error
} }
func DeleteTagById(id string) error {
result := DB.Where("id=?", id).Delete(&Tag{})
return result.Error
}
func GetAllPodcastItemsByPodcastId(podcastId string, podcastItems *[]PodcastItem) error { func GetAllPodcastItemsByPodcastId(podcastId string, podcastItems *[]PodcastItem) error {
result := DB.Preload(clause.Associations).Where(&PodcastItem{PodcastID: podcastId}).Find(&podcastItems) result := DB.Preload(clause.Associations).Where(&PodcastItem{PodcastID: podcastId}).Find(&podcastItems)
return result.Error return result.Error
} }
func GetAllPodcastItemsByPodcastIds(podcastIds []string, podcastItems *[]PodcastItem) error {
result := DB.Preload(clause.Associations).Where("podcast_id in ?", podcastIds).Order("pub_date desc").Find(&podcastItems)
return result.Error
}
func SetAllEpisodesToDownload(podcastId string) error { func SetAllEpisodesToDownload(podcastId string) error {
result := DB.Debug().Model(PodcastItem{}).Where(&PodcastItem{PodcastID: podcastId, DownloadStatus: Deleted}).Update("download_status", NotDownloaded) result := DB.Debug().Model(PodcastItem{}).Where(&PodcastItem{PodcastID: podcastId, DownloadStatus: Deleted}).Update("download_status", NotDownloaded)
@ -260,6 +280,12 @@ func GetTagById(id string) (*Tag, error) {
return &tag, result.Error return &tag, result.Error
} }
func GetTagsByIds(ids []string) (*[]Tag, error) {
var tag []Tag
result := DB.Preload(clause.Associations).Where("id in ?", ids).Find(&tag)
return &tag, result.Error
}
func GetTagByLabel(label string) (*Tag, error) { func GetTagByLabel(label string) (*Tag, error) {
var tag Tag var tag Tag
result := DB.Preload(clause.Associations). result := DB.Preload(clause.Associations).
@ -284,3 +310,8 @@ func RemoveTagFromPodcast(id, tagId string) error {
tx := DB.Debug().Exec("DELETE FROM `podcast_tags` WHERE `podcast_id`=? AND `tag_id`=?", id, tagId) tx := DB.Debug().Exec("DELETE FROM `podcast_tags` WHERE `podcast_id`=? AND `tag_id`=?", id, tagId)
return tx.Error return tx.Error
} }
func UntagAllByTagId(tagId string) error {
tx := DB.Debug().Exec("DELETE FROM `podcast_tags` WHERE `tag_id`=?", tagId)
return tx.Error
}

@ -350,7 +350,7 @@ func cleanAttributes(a []parser.Attribute, allowed []string) []parser.Attribute
// A list of characters we consider separators in normal strings and replace with our canonical separator - rather than removing. // A list of characters we consider separators in normal strings and replace with our canonical separator - rather than removing.
var ( var (
separators = regexp.MustCompile(`[!&_="+?:]`) separators = regexp.MustCompile(`[!&_="|+?:]`)
dashes = regexp.MustCompile(`[\-]+`) dashes = regexp.MustCompile(`[\-]+`)
) )

@ -126,6 +126,7 @@ func main() {
router.GET("/tags", controllers.GetAllTags) router.GET("/tags", controllers.GetAllTags)
router.GET("/tags/:id", controllers.GetTagById) router.GET("/tags/:id", controllers.GetTagById)
router.DELETE("/tags/:id", controllers.DeleteTagById)
router.POST("/tags", controllers.AddTag) router.POST("/tags", controllers.AddTag)
router.POST("/podcasts/:id/tags/:tagId", controllers.AddTagToPodcast) router.POST("/podcasts/:id/tags/:tagId", controllers.AddTagToPodcast)
router.DELETE("/podcasts/:id/tags/:tagId", controllers.RemoveTagFromPodcast) router.DELETE("/podcasts/:id/tags/:tagId", controllers.RemoveTagFromPodcast)
@ -135,6 +136,7 @@ func main() {
router.GET("/", controllers.HomePage) router.GET("/", controllers.HomePage)
router.GET("/podcasts/:id/view", controllers.PodcastPage) router.GET("/podcasts/:id/view", controllers.PodcastPage)
router.GET("/episodes", controllers.AllEpisodesPage) router.GET("/episodes", controllers.AllEpisodesPage)
router.GET("/allTags", controllers.AllTagsPage)
router.GET("/settings", controllers.SettingsPage) router.GET("/settings", controllers.SettingsPage)
router.POST("/settings", controllers.UpdateSetting) router.POST("/settings", controllers.UpdateSetting)
router.GET("/backups", controllers.BackupsPage) router.GET("/backups", controllers.BackupsPage)

@ -58,6 +58,19 @@ func GetPodcastItemById(id string) *db.PodcastItem {
return &podcastItem return &podcastItem
} }
func GetAllPodcastItemsByPodcastIds(podcastIds []string) *[]db.PodcastItem {
var podcastItems []db.PodcastItem
db.GetAllPodcastItemsByPodcastIds(podcastIds, &podcastItems)
return &podcastItems
}
func GetTagsByIds(ids []string) *[]db.Tag {
tags, _ := db.GetTagsByIds(ids)
return tags
}
func GetAllPodcasts(sorting string) *[]db.Podcast { func GetAllPodcasts(sorting string) *[]db.Podcast {
var podcasts []db.Podcast var podcasts []db.Podcast
db.GetAllPodcasts(&podcasts, sorting) db.GetAllPodcasts(&podcasts, sorting)
@ -588,6 +601,15 @@ func DeletePodcast(id string, deleteFiles bool) error {
} }
return nil return nil
}
func DeleteTag(id string) error {
db.UntagAllByTagId(id)
err := db.DeleteTagById(id)
if err != nil {
return err
}
return nil
} }
func makeQuery(url string) ([]byte, error) { func makeQuery(url string) ([]byte, error) {

Loading…
Cancel
Save