Merge pull request #53 from akhilrex/ta

swagger
Akhil Gupta 4 years ago committed by GitHub
commit afd1696bfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -53,6 +53,15 @@
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
} }
.grid .tags{
font-size: 0.85em;
padding-bottom: 10px;
display: inline-block;
}
.mobile.tags{
margin-bottom: 10px;
}
.alignRight{ .alignRight{
text-align: right; text-align: right;
@ -60,7 +69,38 @@
.alignLeft{ .alignLeft{
text-align: left; text-align: left;
} }
.tag-editor{
position: absolute;
border:1px solid;
padding: 10px;
width: 300px;
}
.tag-editor a.pill{
margin-right: 5px;
background: blue;
border-radius: 5px;
padding: 2px 5px;
text-decoration: none;
display: inline-block;
margin-bottom: 5px;
}
a.pill i{
background-color: inherit;
margin-right: 2px;
}
.tag-editor .available a.pill{
background: green;
}
.tag-editor .existing a.pill{
background: palevioletred;
}
.tag-editor>div{
margin-bottom: 15px;;
}
</style> </style>
</head> </head>
<body> <body>
@ -81,6 +121,14 @@
</select> </select>
<select v-model="filterTag" name="" id="">
<option value="">All</option>
<option v-for="option in allTags" v-bind:value="option.ID">
${option.Label} (${option.Podcasts.length})
</option>
</select>
<select v-if="!isMobile" v-model="layout" name="" id=""> <select v-if="!isMobile" v-model="layout" name="" id="">
<option v-for="option in layoutOptions" v-bind:value="option"> <option v-for="option in layoutOptions" v-bind:value="option">
${option.capitalize()} ${option.capitalize()}
@ -120,19 +168,23 @@
<p class="useMore">${podcast.Summary}</p></div> <p class="useMore">${podcast.Summary}</p></div>
<div class="row"> <div class="row">
<div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}"> <div class="columns" v-bind:class="{two:layout=='list', twelve:layout=='grid'}">
<span v-if="podcast.LastEpisode" :title="'Last Episode aired on '+getFormattedLastEpisodeDate(podcast)">${getFormattedLastEpisodeDate(podcast)}</span> <span v-if="podcast.LastEpisode" :title="'Last Episode aired on '+getFormattedLastEpisodeDate(podcast)">${getFormattedLastEpisodeDate(podcast)}</span>
</div> </div>
<div <div
class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}" class="columns" v-bind:class="{two:layout=='list', twelve:layout=='grid'}"
:title="getEpisodeCountTooltip(podcast)" :title="getEpisodeCountTooltip(podcast)"
> >
<template v-if="podcast.DownloadingEpisodesCount"> <template v-if="podcast.DownloadingEpisodesCount">
(${podcast.DownloadingEpisodesCount})/</template>${podcast.DownloadedEpisodesCount}/${podcast.AllEpisodesCount} (${podcast.DownloadingEpisodesCount})/</template>${podcast.DownloadedEpisodesCount}/${podcast.AllEpisodesCount}
episodes episodes
</div> </div>
<div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}">
<tagger :class="isMobile?'mobile':'desktop'" v-bind:podcast="podcast" v-on:getalltags="getAllTags()"></tagger>
</div>
<div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}"> <div class="columns" v-bind:class="{four:layout=='list', twelve:layout=='grid'}">
<button <button
class="button button-delete deletePodcast" class="button button-delete deletePodcast"
@ -194,11 +246,16 @@
</template> </template>
</div> </div>
<template v-if="allPodcasts.length && !podcasts.length">
<template v-if="!podcasts.length"> <div class="welcome">
<h5>No results!</h5>
<p>There doesn't seem to be any podcast for this filter criteria.</p></div>
</template>
</template>
<template v-if="!allPodcasts.length">
<div class="welcome"> <div class="welcome">
<h5>Welcome</h5> <h5>Welcome</h5>
<p>It seems you have just setup Podgrab for the first time?</p> <p>It seems you have just setup Podgrab for the first time.</p>
<p> <p>
Before you start adding and downloading podcasts I recommend that you Before you start adding and downloading podcasts I recommend that you
give a quick look to the <a href="/settings"><strong>Settings</strong> here</a> so that you can customize the give a quick look to the <a href="/settings"><strong>Settings</strong> here</a> so that you can customize the
@ -219,6 +276,137 @@
<script src="/webassets/popper.min.js"></script> <script src="/webassets/popper.min.js"></script>
<script src="/webassets/tippy-bundle.umd.min.js"></script> <script src="/webassets/tippy-bundle.umd.min.js"></script>
<template id="editTags">
<div class="tags">
<div @click="editTags" style="cursor: pointer;">
<i class="fas fa-tags"></i>
${ commaSeparatedTags() }
</div>
<div v-if="editing" class="tag-editor">
<h5>Tags: ${podcast.Title}</h5>
<div class="available">
Add: <a style="cursor: pointer;" href="#" @click.prevent="addTag(tag.ID, $event);return false;" v-for="tag in availableTags" class="pill"><i class="fa fa-plus-circle"></i>${tag.Label}</a>
</div>
<div class="existing" v-if="tags.length">
Remove: <a style="cursor: pointer;" @click.prevent="removeTag(tag.ID, $event);return false;" href="#" v-for="tag in tags" class="pill"><i class="fa fa-minus-circle"></i>${tag.Label}</a>
</div>
<div class="create">
<form @submit.prevent="createNewTag" method="post">
<input type="text" name="newTag" id="" placeholder="Create New Tag" v-model="newTag">
<input type="submit" value="Add">
</form>
</div>
<button class="button" @click="editing=false">Close</button>
</div>
</div>
</template>
<script>
Vue.component('tagger',{
delimiters: ["${", "}"],
data:function(){
return {
newTag:'',
allTags:[],
tags:[],
availableTags:[],
editing:false,
}
},
template: '#editTags',
props:['podcast'],
computed:{
},
methods:{
createNewTag(){
var self=this;
if(!self.newTag){
return;
}
axios
.post("/tags",{label:self.newTag})
.then(function (response) {
self.tags.push(response.data);
self.addTag(response.data.ID);
self.getAllTags();
}).catch(showError);
},
setAvailableTags(){
existingTags= this.tags.map(x=>x.ID);
this.availableTags= this.allTags.filter(x=>existingTags.indexOf(x.ID)===-1);
this.$emit('getalltags')
}
,
commaSeparatedTags(){
if(!this.tags.length){
return "";
}
toReturn= this.tags.map(function(x){return x.Label}).join(", ");
return toReturn;
},
editTags(){
this.editing=!this.editing;
if(this.editing){
this.getAllTags();
}
},
getAllTags(){
var self=this;
axios
.get("/tags")
.then(function (response) {
self.allTags=response.data;
self.setAvailableTags();
})
},
addTag(tagId,e){
var self=this;
axios
.post("/podcasts/"+this.podcast.ID+"/tags/"+tagId)
.then(function (response) {
var i=-1;
for(i=0;i<self.allTags.length;i++){
if(self.allTags[i].ID===tagId){
self.tags.push(self.allTags[i]);
break;
}
}
self.setAvailableTags();
}).catch(showError);
return false;
},
removeTag(tagId,e){
var self=this;
axios
.delete("/podcasts/"+this.podcast.ID+"/tags/"+tagId)
.then(function (response) {
var i=-1;
for(i=0;i<self.tags.length;i++){
if(self.tags[i].ID===tagId){
break;
}
}
self.tags.splice(i,1)
self.setAvailableTags();
});
return false;
},
},
mounted(){
this.tags=this.podcast.Tags;
}
});
</script>
<script> <script>
var app = new Vue({ var app = new Vue({
delimiters: ["${", "}"], delimiters: ["${", "}"],
@ -235,6 +423,7 @@
} }
}, },
created(){ created(){
this.podcasts=this.allPodcasts;
const self=this; const self=this;
this.socket= getWebsocketConnection(function(event){ this.socket= getWebsocketConnection(function(event){
const message= getWebsocketMessage("Register","Home") const message= getWebsocketMessage("Register","Home")
@ -248,6 +437,7 @@
self.playerExists=true; self.playerExists=true;
} }
}); });
}, },
methods:{ methods:{
removePodcast(id) { removePodcast(id) {
@ -260,6 +450,23 @@
} }
this.socket.send(getWebsocketMessage("Enqueue",`{"podcastId":"${id}"}`)) this.socket.send(getWebsocketMessage("Enqueue",`{"podcastId":"${id}"}`))
}, },
filterPodcasts(){
if(this.filterTag===""){
this.podcasts=this.allPodcasts;
}else{
var filtered=[];
for (var podast of this.allPodcasts) {
for(var tag of podast.Tags){
if(tag.ID===this.filterTag){
filtered.push(podast);
break;
}
}
}
this.podcasts=filtered;
}
this.sortPodcasts();
},
sortPodcasts(order){ sortPodcasts(order){
var compareFunction; var compareFunction;
switch(order){ switch(order){
@ -319,6 +526,14 @@
});}, });},
deletePodcastEpisodes(id){ deletePodcastEpisodes(id)}, deletePodcastEpisodes(id){ deletePodcastEpisodes(id)},
playPodcast(id){openPlayer("",id)}, playPodcast(id){openPlayer("",id)},
getAllTags(){
var self=this;
axios
.get("/tags")
.then(function (response) {
self.allTags=response.data;
})
},
}, },
mounted(){ mounted(){
if(localStorage && localStorage.sortOrder){ if(localStorage && localStorage.sortOrder){
@ -331,6 +546,12 @@
this.layout='list'; this.layout='list';
} }
if(localStorage && localStorage.filterTag){
this.filterTag=localStorage.filterTag;
}else{
this.filterTag='';
}
if (screen.width <= 760) { if (screen.width <= 760) {
this.isMobile= true this.isMobile= true
} else { } else {
@ -340,6 +561,7 @@
if (screen.width <= 760) { if (screen.width <= 760) {
this.layout='list' this.layout='list'
} }
this.getAllTags();
this.$nextTick(function () { this.$nextTick(function () {
@ -369,6 +591,16 @@
} }
this.sortPodcasts(newOrder); this.sortPodcasts(newOrder);
}, },
filterTag(newTag,oldTag){
if(newTag===oldTag){
return;
}
if(localStorage){
localStorage.filterTag=newTag
}
this.filterPodcasts();
},
layout(newLayout,oldLayout){ layout(newLayout,oldLayout){
if(newLayout===oldLayout){ if(newLayout===oldLayout){
return; return;
@ -385,6 +617,8 @@
layoutOptions:["list","grid"], layoutOptions:["list","grid"],
layout:"grid", layout:"grid",
sortOrder:"dateAdded-asc", sortOrder:"dateAdded-asc",
allTags:[],
filterTag:'',
sortOptions:[ sortOptions:[
{ {
key:"name-asc", key:"name-asc",
@ -412,14 +646,12 @@
label:"Date Added (New First)" label:"Date Added (New First)"
}, },
], ],
podcasts:[],
{{ $len := len .podcasts}} {{ $len := len .podcasts}}
podcasts: {{if gt $len 0}} {{ .podcasts }} {{else}} [] {{end}}, allPodcasts: {{if gt $len 0}} {{ .podcasts }} {{else}} [] {{end}},
}}) }})
</script> </script>
<script>
</script>
</body> </body>
</html> </html>

@ -246,5 +246,23 @@
.then(function () {}); .then(function () {});
return false; return false;
} }
function showError(error){
var message="An error has occured."
if(typeof(error)==="string"){
message=error;
}
if (error.response && error.response.data && error.response.data.message) {
message=error.response.data.message;
}
console.log(error)
Vue.toasted.show(message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
</script> </script>
{{end}} {{end}}

@ -38,6 +38,11 @@ type SearchByIdQuery struct {
Id string `binding:"required" uri:"id" json:"id" form:"id"` Id string `binding:"required" uri:"id" json:"id" form:"id"`
} }
type AddRemoveTagQuery struct {
Id string `binding:"required" uri:"id" json:"id" form:"id"`
TagId string `binding:"required" uri:"tagId" json:"tagId" form:"tagId"`
}
type Pagination struct { type Pagination struct {
Page int `uri:"page" query:"page" json:"page" form:"page"` Page int `uri:"page" query:"page" json:"page" form:"page"`
Count int `uri:"count" query:"count" json:"count" form:"count"` Count int `uri:"count" query:"count" json:"count" form:"count"`
@ -57,6 +62,10 @@ type PatchPodcastItem struct {
type AddPodcastData struct { type AddPodcastData struct {
Url string `binding:"required" form:"url" json:"url"` Url string `binding:"required" form:"url" json:"url"`
} }
type AddTagData struct {
Label string `binding:"required" form:"label" json:"label"`
Description string `form:"description" json:"description"`
}
func GetAllPodcasts(c *gin.Context) { func GetAllPodcasts(c *gin.Context) {
var podcastListQuery PodcastListQuery var podcastListQuery PodcastListQuery
@ -299,6 +308,74 @@ func AddPodcast(c *gin.Context) {
} }
} }
func GetAllTags(c *gin.Context) {
tags, err := db.GetAllTags("")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
} else {
c.JSON(200, tags)
}
}
func GetTagById(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
tag, err := db.GetTagById(searchByIdQuery.Id)
if err == nil {
c.JSON(200, tag)
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func AddTag(c *gin.Context) {
var addTagData AddTagData
err := c.ShouldBindJSON(&addTagData)
if err == nil {
tag, err := service.AddTag(addTagData.Label, addTagData.Description)
if err == nil {
c.JSON(200, tag)
} else {
if v, ok := err.(*model.TagAlreadyExistsError); ok {
c.JSON(409, gin.H{"message": v.Error()})
} else {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
}
}
} else {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
}
}
func AddTagToPodcast(c *gin.Context) {
var addRemoveTagQuery AddRemoveTagQuery
if c.ShouldBindUri(&addRemoveTagQuery) == nil {
err := db.AddTagToPodcast(addRemoveTagQuery.Id, addRemoveTagQuery.TagId)
if err == nil {
c.JSON(200, gin.H{})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func RemoveTagFromPodcast(c *gin.Context) {
var addRemoveTagQuery AddRemoveTagQuery
if c.ShouldBindUri(&addRemoveTagQuery) == nil {
err := db.RemoveTagFromPodcast(addRemoveTagQuery.Id, addRemoveTagQuery.TagId)
if err == nil {
c.JSON(200, gin.H{})
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func UpdateSetting(c *gin.Context) { func UpdateSetting(c *gin.Context) {
var model SettingModel var model SettingModel
err := c.ShouldBind(&model) err := c.ShouldBind(&model)

@ -35,7 +35,7 @@ func Init() (*gorm.DB, error) {
//Migrate Database //Migrate Database
func Migrate() { func Migrate() {
DB.AutoMigrate(&Podcast{}, &PodcastItem{}, &Setting{}, &Migration{}, &JobLock{}) DB.AutoMigrate(&Podcast{}, &PodcastItem{}, &Setting{}, &Migration{}, &JobLock{}, &Tag{})
RunMigrations() RunMigrations()
} }

@ -23,7 +23,7 @@ func GetAllPodcasts(podcasts *[]Podcast, sorting string) error {
if sorting == "" { if sorting == "" {
sorting = "created_at" sorting = "created_at"
} }
result := DB.Debug().Order(sorting).Find(&podcasts) result := DB.Debug().Preload("Tags").Order(sorting).Find(&podcasts)
return result.Error return result.Error
} }
func GetAllPodcastItems(podcasts *[]PodcastItem) error { func GetAllPodcastItems(podcasts *[]PodcastItem) error {
@ -243,3 +243,44 @@ func UnlockMissedJobs() {
} }
} }
} }
func GetAllTags(sorting string) (*[]Tag, error) {
var tags []Tag
if sorting == "" {
sorting = "created_at"
}
result := DB.Debug().Preload(clause.Associations).Order(sorting).Find(&tags)
return &tags, result.Error
}
func GetTagById(id string) (*Tag, error) {
var tag Tag
result := DB.Preload(clause.Associations).
First(&tag, "id=?", id)
return &tag, result.Error
}
func GetTagByLabel(label string) (*Tag, error) {
var tag Tag
result := DB.Preload(clause.Associations).
First(&tag, "label=?", label)
return &tag, result.Error
}
func CreateTag(tag *Tag) error {
tx := DB.Debug().Omit("Podcasts").Create(&tag)
return tx.Error
}
func UpdateTag(tag *Tag) error {
tx := DB.Omit("Podcast").Save(&tag)
return tx.Error
}
func AddTagToPodcast(id, tagId string) error {
tx := DB.Debug().Exec("INSERT INTO `podcast_tags` (`podcast_id`,`tag_id`) VALUES (?,?) ON CONFLICT DO NOTHING", id, tagId)
return tx.Error
}
func RemoveTagFromPodcast(id, tagId string) error {
tx := DB.Debug().Exec("DELETE FROM `podcast_tags` WHERE `podcast_id`=? AND `tag_id`=?", id, tagId)
return tx.Error
}

@ -21,6 +21,8 @@ type Podcast struct {
PodcastItems []PodcastItem PodcastItems []PodcastItem
Tags []*Tag `gorm:"many2many:podcast_tags;"`
DownloadedEpisodesCount int `gorm:"-"` DownloadedEpisodesCount int `gorm:"-"`
DownloadingEpisodesCount int `gorm:"-"` DownloadingEpisodesCount int `gorm:"-"`
AllEpisodesCount int `gorm:"-"` AllEpisodesCount int `gorm:"-"`
@ -88,6 +90,13 @@ type JobLock struct {
Duration int Duration int
} }
type Tag struct {
Base
Label string
Description string `gorm:"type:text"`
Podcasts []*Podcast `gorm:"many2many:podcast_tags;"`
}
func (lock *JobLock) IsLocked() bool { func (lock *JobLock) IsLocked() bool {
return lock != nil && lock.Date != time.Time{} return lock != nil && lock.Date != time.Time{}
} }

@ -123,6 +123,12 @@ func main() {
router.GET("/podcastitems/:id/download", controllers.DownloadPodcastItem) router.GET("/podcastitems/:id/download", controllers.DownloadPodcastItem)
router.GET("/podcastitems/:id/delete", controllers.DeletePodcastItem) router.GET("/podcastitems/:id/delete", controllers.DeletePodcastItem)
router.GET("/tags", controllers.GetAllTags)
router.GET("/tags/:id", controllers.GetTagById)
router.POST("/tags", controllers.AddTag)
router.POST("/podcasts/:id/tags/:tagId", controllers.AddTagToPodcast)
router.DELETE("/podcasts/:id/tags/:tagId", controllers.RemoveTagFromPodcast)
router.GET("/add", controllers.AddPage) router.GET("/add", controllers.AddPage)
router.GET("/search", controllers.Search) router.GET("/search", controllers.Search)
router.GET("/", controllers.HomePage) router.GET("/", controllers.HomePage)

@ -9,3 +9,11 @@ type PodcastAlreadyExistsError struct {
func (e *PodcastAlreadyExistsError) Error() string { func (e *PodcastAlreadyExistsError) Error() string {
return fmt.Sprintf("Podcast with this url already exists") return fmt.Sprintf("Podcast with this url already exists")
} }
type TagAlreadyExistsError struct {
Label string
}
func (e *TagAlreadyExistsError) Error() string {
return fmt.Sprintf("Tag with this label already exists : " + e.Label)
}

@ -632,3 +632,22 @@ func UpdateSettings(downloadOnAdd bool, initialDownloadCount int, autoDownload b
func UnlockMissedJobs() { func UnlockMissedJobs() {
db.UnlockMissedJobs() db.UnlockMissedJobs()
} }
func AddTag(label, description string) (db.Tag, error) {
tag, err := db.GetTagByLabel(label)
if errors.Is(err, gorm.ErrRecordNotFound) {
tag := db.Tag{
Label: label,
Description: description,
}
err = db.CreateTag(&tag)
return tag, err
}
return *tag, &model.TagAlreadyExistsError{Label: label}
}

Loading…
Cancel
Save