ability to add,search and delete podcasts

pull/24/head
Akhil Gupta 4 years ago
parent c59ba5198d
commit 6788bf587b

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
<title>Add Podcast - PodGrab</title>
{{template "commoncss"}}
<style>
[v-cloak] { display: none }
</style>
</head>
<body>
<div class="container">
<section class="header">
<h1>{{ .title }}</h1>
</section>
{{template "navbar"}}
<br>
<div id="app" v-cloak>
<div class="row">
<h4>Add using the direct link to rss feed</h4>
<form action="/" method="post" @submit="addPodcastManual">
<div class="nine columns">
<input type="url" v-model="url" name="url" id="url" placeholder="Enter Podcast RSS feed to add" class="u-full-width">
</div>
<div class="three columns">
<input type="submit" value="Add Podcast" class="u-full-width button-primary">
</div>
</form>
</div>
<hr>
<div class="row" id="searchContainer">
<h4>Search for your favorite podcast</h4>
<form action="/search" method="post" @submit="search">
<div class="nine columns">
<input type="search" name="search" id="search" placeholder="Search for your podcast" v-model="query" class="u-full-width">
</div>
<div class="three columns">
<input type="submit" value="Search" class="u-full-width button-primary">
</div>
</form>
<br>
<progress v-if="searching" class="u-full-width"></progress>
<div class="results">
<div v-for="item in results" :key="item.url" class="row">
<div class="columns two">
<img class="u-full-width" :src="item.scaled_logo_url" :alt="item.title">
</div>
<div class="columns nine">
<h5>${item.title}</h5>
<p>${ item.description }</p>
</div>
<div class="columns one">
<button v-if="!item.already_saved" v-on:click="addPodcast(item)" class="button button-primary">+ Add</button >
</div>
</div>
</div>
</div>
</div>
</div>
{{template "scripts"}}
<script>
var app = new Vue({
delimiters: ['${', '}'],
el: '#app',
data: {
results: [],
query:'',
searching:false,
url:''
},
methods:{
search:function(e){
e.preventDefault();
if(!this.query){
return;
}
var self=this;
self.searching=true;
axios.get("/search?q="+this.query)
.then(function(response){
self.results= response.data
})
.catch(function(error){
}).
then(function(){
self.searching=false;
})
},
addPodcastManual:function(e){
e.preventDefault();
if(!this.url){
return;
}
this.addPodcast({url:this.url})
}
,
addPodcast:function(item){
// console.log(item);
var self=this;
self.searching=true;
axios.post("/podcasts",{
url:item.url
})
.then(function(response){
Vue.toasted.show('Podcast added successfully.' ,{
theme: "bubble",
position: "top-right",
duration : 5000
})
item.already_saved= true
})
.catch(function(error){
if(error.response){
Vue.toasted.show(error.response.data?.message, {
theme: "bubble",
position: "top-right",
duration : 5000
})
}
}).
then(function(){
self.searching=false;
})
return false;
}
}
})
</script>
</body>
</html>

@ -0,0 +1,120 @@
{{define "commoncss"}}
<style>
.container {
max-width: 800px; }
.header {
margin-top: 6rem;
text-align: center; }
.value-prop {
margin-top: 1rem; }
.value-props {
margin-top: 4rem;
margin-bottom: 4rem; }
.docs-header {
text-transform: uppercase;
font-size: 1.4rem;
letter-spacing: .2rem;
font-weight: 600; }
.docs-section {
border-top: 1px solid #eee;
padding: 4rem 0;
margin-bottom: 0;}
.value-img {
display: block;
text-align: center;
margin: 2.5rem auto 0; }
.example-grid .column,
.example-grid .columns {
background: #EEE;
text-align: center;
border-radius: 4px;
font-size: 1rem;
text-transform: uppercase;
height: 30px;
line-height: 30px;
margin-bottom: .75rem;
font-weight: 600;
letter-spacing: .1rem; }
.docs-example .row,
.docs-example.row,
.docs-example form {
margin-bottom: 0; }
.docs-example h1,
.docs-example h2,
.docs-example h3,
.docs-example h4,
.docs-example h5,
.docs-example h6 {
margin-bottom: 1rem; }
.heading-font-size {
font-size: 1.2rem;
color: #999;
letter-spacing: normal; }
.code-example {
margin-top: 1.5rem;
margin-bottom: 0; }
.code-example-body {
white-space: pre;
word-wrap: break-word }
.example {
position: relative;
margin-top: 4rem; }
.example-header {
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: .5rem; }
.example-description {
margin-bottom: 1.5rem; }
.example-screenshot-wrapper {
display: block;
position: relative;
overflow: hidden;
border-radius: 6px;
border: 1px solid #eee;
height: 250px; }
.example-screenshot {
width: 100%;
height: auto; }
.example-screenshot.coming-soon {
width: auto;
position: absolute;
background: #eee;
top: 5px;
right: 5px;
bottom: 5px;
left: 5px; }
.navbar {
display: none; }
/* Larger than phone */
@media (min-width: 550px) {
.header {
margin-top: 8rem;
}
.value-props {
margin-top: 9rem;
margin-bottom: 7rem; }
.value-img {
margin-bottom: 1rem; }
.example-grid .column,
.example-grid .columns {
margin-bottom: 1.5rem; }
.docs-section {
padding: 6rem 0; }
.example-send-yourself-copy {
float: right;
margin-top: 12px; }
.example-screenshot-wrapper {
position: absolute;
width: 48%;
height: 100%;
left: 0;
max-height: none; }
}
/* Larger than tablet */
@media (min-width: 750px) {
}
</style>
{{end}}

@ -5,125 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
<title>PodGrab</title> <title>PodGrab</title>
{{template "commoncss"}}
<style> <style>
.container {
max-width: 800px; }
.header {
margin-top: 6rem;
text-align: center; }
.value-prop {
margin-top: 1rem; }
.value-props {
margin-top: 4rem;
margin-bottom: 4rem; }
.docs-header {
text-transform: uppercase;
font-size: 1.4rem;
letter-spacing: .2rem;
font-weight: 600; }
.docs-section {
border-top: 1px solid #eee;
padding: 4rem 0;
margin-bottom: 0;}
.value-img {
display: block;
text-align: center;
margin: 2.5rem auto 0; }
.example-grid .column,
.example-grid .columns {
background: #EEE;
text-align: center;
border-radius: 4px;
font-size: 1rem;
text-transform: uppercase;
height: 30px;
line-height: 30px;
margin-bottom: .75rem;
font-weight: 600;
letter-spacing: .1rem; }
.docs-example .row,
.docs-example.row,
.docs-example form {
margin-bottom: 0; }
.docs-example h1,
.docs-example h2,
.docs-example h3,
.docs-example h4,
.docs-example h5,
.docs-example h6 {
margin-bottom: 1rem; }
.heading-font-size {
font-size: 1.2rem;
color: #999;
letter-spacing: normal; }
.code-example {
margin-top: 1.5rem;
margin-bottom: 0; }
.code-example-body {
white-space: pre;
word-wrap: break-word }
.example {
position: relative;
margin-top: 4rem; }
.example-header {
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: .5rem; }
.example-description {
margin-bottom: 1.5rem; }
.example-screenshot-wrapper {
display: block;
position: relative;
overflow: hidden;
border-radius: 6px;
border: 1px solid #eee;
height: 250px; }
.example-screenshot {
width: 100%;
height: auto; }
.example-screenshot.coming-soon {
width: auto;
position: absolute;
background: #eee;
top: 5px;
right: 5px;
bottom: 5px;
left: 5px; }
.navbar {
display: none; }
img{ img{
display: none display: none
} }
/* Larger than phone */
@media (min-width: 550px) {
.header {
margin-top: 8rem;
}
.value-props {
margin-top: 9rem;
margin-bottom: 7rem; }
.value-img {
margin-bottom: 1rem; }
.example-grid .column,
.example-grid .columns {
margin-bottom: 1.5rem; }
.docs-section {
padding: 6rem 0; }
.example-send-yourself-copy {
float: right;
margin-top: 12px; }
.example-screenshot-wrapper {
position: absolute;
width: 48%;
height: 100%;
left: 0;
max-height: none; }
img{
display: block
}
}
h1,h2,h3,h4,h5{ h1,h2,h3,h4,h5{
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -167,9 +55,9 @@ hr{
<h4>{{.Title}} // {{ .Podcast.Title}}</h4> <h4>{{.Title}} // {{ .Podcast.Title}}</h4>
<small>{{ formatDate .PubDate }}</small> <small>{{ formatDate .PubDate }}</small>
<p>{{ .Summary }}</p> <p>{{ .Summary }}</p>
{{if .DownloadPath}}
<a class="button button-primary" href="{{ .DownloadPath }}" download>Download</a> <a class="button button-primary" href="{{ .DownloadPath }}" download>Download</a>
{{end }}
</div> </div>
<div class="columns one"> <div class="columns one">

@ -5,124 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
<title>PodGrab</title> <title>PodGrab</title>
{{template "commoncss"}}
<style> <style>
.container { .button-delete{
max-width: 800px; } background-color: indianred;
.header { color:wheat;
margin-top: 6rem;
text-align: center; }
.value-prop {
margin-top: 1rem; }
.value-props {
margin-top: 4rem;
margin-bottom: 4rem; }
.docs-header {
text-transform: uppercase;
font-size: 1.4rem;
letter-spacing: .2rem;
font-weight: 600; }
.docs-section {
border-top: 1px solid #eee;
padding: 4rem 0;
margin-bottom: 0;}
.value-img {
display: block;
text-align: center;
margin: 2.5rem auto 0; }
.example-grid .column,
.example-grid .columns {
background: #EEE;
text-align: center;
border-radius: 4px;
font-size: 1rem;
text-transform: uppercase;
height: 30px;
line-height: 30px;
margin-bottom: .75rem;
font-weight: 600;
letter-spacing: .1rem; }
.docs-example .row,
.docs-example.row,
.docs-example form {
margin-bottom: 0; }
.docs-example h1,
.docs-example h2,
.docs-example h3,
.docs-example h4,
.docs-example h5,
.docs-example h6 {
margin-bottom: 1rem; }
.heading-font-size {
font-size: 1.2rem;
color: #999;
letter-spacing: normal; }
.code-example {
margin-top: 1.5rem;
margin-bottom: 0; }
.code-example-body {
white-space: pre;
word-wrap: break-word }
.example {
position: relative;
margin-top: 4rem; }
.example-header {
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: .5rem; }
.example-description {
margin-bottom: 1.5rem; }
.example-screenshot-wrapper {
display: block;
position: relative;
overflow: hidden;
border-radius: 6px;
border: 1px solid #eee;
height: 250px; }
.example-screenshot {
width: 100%;
height: auto; }
.example-screenshot.coming-soon {
width: auto;
position: absolute;
background: #eee;
top: 5px;
right: 5px;
bottom: 5px;
left: 5px; }
.navbar {
display: none; }
/* Larger than phone */
@media (min-width: 550px) {
.header {
margin-top: 18rem;
} }
.value-props {
margin-top: 9rem;
margin-bottom: 7rem; }
.value-img {
margin-bottom: 1rem; }
.example-grid .column,
.example-grid .columns {
margin-bottom: 1.5rem; }
.docs-section {
padding: 6rem 0; }
.example-send-yourself-copy {
float: right;
margin-top: 12px; }
.example-screenshot-wrapper {
position: absolute;
width: 48%;
height: 100%;
left: 0;
max-height: none; }
}
/* Larger than tablet */
@media (min-width: 750px) {
}
</style> </style>
</head> </head>
<body> <body>
@ -131,7 +19,7 @@
<h1>{{ .title }}</h1> <h1>{{ .title }}</h1>
</section> </section>
{{template "navbar"}} {{template "navbar"}}
<div class="row"> <div class="row" style="display: none;">
<form action="/" method="post"> <form action="/" method="post">
<div class="nine columns"> <div class="nine columns">
<input type="url" name="url" id="url" placeholder="Enter Podcast RSS feed to add" class="u-full-width"> <input type="url" name="url" id="url" placeholder="Enter Podcast RSS feed to add" class="u-full-width">
@ -141,29 +29,77 @@
</form> </form>
</div> </div>
<br> <br>
{{range .podcasts}} {{range .podcasts}}
<div class="podcasts row"> <div class="podcasts row" id="podcast-{{ .ID }}">
<div class="columns three"> <div class="columns two">
<img class="u-full-width" src="{{ .Image }}" alt="{{ .Title }}"> <img class="u-full-width" src="{{ .Image }}" alt="{{ .Title }}">
</div> </div>
<div class="columns eight"> <div class="columns ten">
<h3>{{.Title}}</h3> <a style="text-decoration: none;" href="/podcasts/{{ .ID }}/view"> <h3>{{.Title}}</h3></a>
<small>Last Epidsode : {{ latestEpisodeDate .PodcastItems}}</small>
<p>{{ .Summary }}</p> <p>{{ .Summary }}</p>
<div class="row">
<div class="columns four">
Last Episode : {{ latestEpisodeDate .PodcastItems}}
</div> </div>
<div class="columns one"> <div class="columns four">
<a href="/podcasts/{{ .ID }}/view"> {{ len .PodcastItems }} episodes</a> {{ len .PodcastItems }} episodes
</div>
</div>
<div class="columns four">
<button class="button button-delete" onclick="deletePodcast('{{.ID}}')">Delete</button>
</div>
</div>
</div>
</div> </div>
<hr> <hr>
{{else}}
<h5>You haven't added any podcasts yet. <a href="/add">Click here</a> to add a new podcast to start monitoring.</h5>
{{end}} {{end}}
<div class="row"> <!-- <div class="row">
<div class="columns twelve" style="text-align: center;"> <div class="columns twelve" style="text-align: center;">
<a href="/episodes" class="button button-primary">All Episodes</a> <a href="/episodes" class="button button-primary">All Episodes</a>
</div> </div>
</div> -->
</div> </div>
</div> {{template "scripts"}}
<script>
function deletePodcast(id){
console.log(id)
var confirmed= confirm("Are you sure you want to delete this podcast?");
if(!confirmed){
return false;
}
axios.delete("/podcasts/"+id)
.then(function(response){
Vue.toasted.show('Podcast deleted successfully.' ,{
theme: "bubble",
position: "top-right",
duration : 5000
});
var row = document.getElementById('podcast-'+id);
row.remove();
})
.catch(function(error){
if(error.response){
Vue.toasted.show(error.response.data?.message, {
theme: "bubble",
position: "top-right",
duration : 5000
})
}
}).
then(function(){
})
return false;
}
</script>
</body> </body>
</html> </html>

@ -51,6 +51,7 @@
<ul class="navbar-list"> <ul class="navbar-list">
<li class="navbar-item"><a class="navbar-link" href="/">Home</a></li> <li class="navbar-item"><a class="navbar-link" href="/">Home</a></li>
<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>
</ul> </ul>
</div> </div>

@ -0,0 +1,9 @@
{{define "scripts"}}
<script src="/webassets/vue.js"></script>
<script src="/webassets/axios.min.js" ></script>
<script src="/webassets/vue-toasted.min.js"></script>
<script>
Vue.use(Toasted)
</script>
{{end}}

@ -0,0 +1,107 @@
package controllers
import (
"fmt"
"net/http"
"strconv"
"github.com/akhilrex/podgrab/db"
"github.com/akhilrex/podgrab/service"
"github.com/gin-gonic/gin"
)
type SearchGPodderData struct {
Q string `binding:"required" form:"q" json:"q" query:"q"`
}
func AddPage(c *gin.Context) {
c.HTML(http.StatusOK, "addPodcast.html", gin.H{"title": "Add Podcast"})
}
func HomePage(c *gin.Context) {
//var podcasts []db.Podcast
podcasts := service.GetAllPodcasts()
c.HTML(http.StatusOK, "index.html", gin.H{"title": "Podgrab", "podcasts": podcasts})
}
func PodcastPage(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
var podcast db.Podcast
if err := db.GetPodcastById(searchByIdQuery.Id, &podcast); err == nil {
c.HTML(http.StatusOK, "podcast.html", gin.H{"title": podcast.Title, "podcast": podcast})
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func AllEpisodesPage(c *gin.Context) {
var pagination Pagination
if c.ShouldBindQuery(&pagination) == nil {
var page, count int
if page = pagination.Page; page == 0 {
page = 1
}
if count = pagination.Count; count == 0 {
count = 10
}
var podcastItems []db.PodcastItem
if err := db.GetPaginatedPodcastItems(page, count, &podcastItems); err == nil {
c.HTML(http.StatusOK, "episodes.html", gin.H{"title": "All Episodes", "podcastItems": podcastItems})
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func Search(c *gin.Context) {
var searchQuery SearchGPodderData
if c.ShouldBindQuery(&searchQuery) == nil {
data := service.Query(searchQuery.Q)
allPodcasts := service.GetAllPodcasts()
urls := make(map[string]string, len(*allPodcasts))
for _, pod := range *allPodcasts {
fmt.Println(pod.URL)
urls[pod.URL] = pod.ID
}
for _, pod := range data {
_, ok := urls[pod.URL]
fmt.Println(pod.URL + " " + strconv.FormatBool(ok))
pod.AlreadySaved = ok
}
c.JSON(200, data)
}
}
func AddNewPodcast(c *gin.Context) {
var addPodcastData AddPodcastData
err := c.ShouldBind(&addPodcastData)
if err == nil {
_, err = service.AddPodcast(addPodcastData.Url)
if err == nil {
go service.RefreshEpisodes()
c.Redirect(http.StatusFound, "/")
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
// fmt.Println(err.Error())
c.JSON(http.StatusBadRequest, err)
}
}

@ -2,8 +2,10 @@ package controllers
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"github.com/akhilrex/podgrab/model"
"github.com/akhilrex/podgrab/service" "github.com/akhilrex/podgrab/service"
"github.com/akhilrex/podgrab/db" "github.com/akhilrex/podgrab/db"
@ -48,6 +50,17 @@ func GetPodcastById(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
} }
} }
func DeletePodcastById(c *gin.Context) {
var searchByIdQuery SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
service.DeletePodcast(searchByIdQuery.Id)
c.JSON(http.StatusNoContent, gin.H{})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
}
func GetPodcastItemsByPodcastId(c *gin.Context) { func GetPodcastItemsByPodcastId(c *gin.Context) {
var searchByIdQuery SearchByIdQuery var searchByIdQuery SearchByIdQuery
@ -89,12 +102,20 @@ func AddPodcast(c *gin.Context) {
var addPodcastData AddPodcastData var addPodcastData AddPodcastData
err := c.ShouldBindJSON(&addPodcastData) err := c.ShouldBindJSON(&addPodcastData)
if err == nil { if err == nil {
pod, err := service.AddPodcast(addPodcastData.Url)
service.AddPodcast(addPodcastData.Url) if err == nil {
// fmt.Println(time.Unix(addPodcastData.StartDate, 0)) go service.RefreshEpisodes()
c.JSON(200, addPodcastData) c.JSON(200, pod)
} else {
if v, ok := err.(*model.PodcastAlreadyExistsError); ok {
c.JSON(409, gin.H{"message": v.Error()})
} else {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
}
}
} else { } else {
fmt.Println(err.Error()) log.Println(err.Error())
c.JSON(http.StatusBadRequest, err) c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
} }
} }

@ -10,6 +10,11 @@ func GetPodcastByURL(url string, podcast *Podcast) error {
result := DB.Preload(clause.Associations).Where(&Podcast{URL: url}).First(&podcast) result := DB.Preload(clause.Associations).Where(&Podcast{URL: url}).First(&podcast)
return result.Error return result.Error
} }
func GetPodcastsByURLList(urls []string, podcasts *[]Podcast) error {
result := DB.Preload(clause.Associations).Where("url in ?", urls).First(&podcasts)
return result.Error
}
func GetAllPodcasts(podcasts *[]Podcast) error { func GetAllPodcasts(podcasts *[]Podcast) error {
result := DB.Preload("PodcastItems").Find(&podcasts) result := DB.Preload("PodcastItems").Find(&podcasts)
@ -35,6 +40,16 @@ func GetPodcastItemById(id string, podcastItem *PodcastItem) error {
result := DB.Preload(clause.Associations).First(&podcastItem, "id=?", id) result := DB.Preload(clause.Associations).First(&podcastItem, "id=?", id)
return result.Error return result.Error
} }
func DeletePodcastItemById(id string) error {
result := DB.Where("id=?", id).Delete(&PodcastItem{})
return result.Error
}
func DeletePodcastById(id string) error {
result := DB.Where("id=?", id).Delete(&Podcast{})
return result.Error
}
func GetAllPodcastItemsByPodcastId(podcastId string, podcasts *[]PodcastItem) error { func GetAllPodcastItemsByPodcastId(podcastId string, podcasts *[]PodcastItem) error {

@ -6,6 +6,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/gobeam/stringy v0.0.0-20200717095810-8a3637503f62 github.com/gobeam/stringy v0.0.0-20200717095810-8a3637503f62
github.com/grokify/html-strip-tags-go v0.0.0-20200923094847-079d207a09f1
github.com/jasonlvhit/gocron v0.0.1 github.com/jasonlvhit/gocron v0.0.1
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/microcosm-cc/bluemonday v1.0.4 github.com/microcosm-cc/bluemonday v1.0.4

@ -34,6 +34,8 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/grokify/html-strip-tags-go v0.0.0-20200923094847-079d207a09f1 h1:ETqBvCd8SQaNCb0TwQ5A+IlkecGuwjW1EUTxK9if+UE=
github.com/grokify/html-strip-tags-go v0.0.0-20200923094847-079d207a09f1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU= github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4= github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
@ -30,7 +29,7 @@ func main() {
} }
r := gin.Default() r := gin.Default()
dataPath := os.Getenv("DATA") dataPath := os.Getenv("DATA")
//r.Static("/assets", "./assets") r.Static("/webassets", "./webassets")
r.Static("/assets", dataPath) r.Static("/assets", dataPath)
funcMap := template.FuncMap{ funcMap := template.FuncMap{
"formatDate": func(raw time.Time) string { "formatDate": func(raw time.Time) string {
@ -51,80 +50,20 @@ func main() {
//r.LoadHTMLGlob("client/*") //r.LoadHTMLGlob("client/*")
r.SetHTMLTemplate(tmpl) r.SetHTMLTemplate(tmpl)
r.GET("/podcasts", controllers.AddPodcast) r.POST("/podcasts", controllers.AddPodcast)
r.POST("/podcasts", controllers.GetAllPodcasts) r.GET("/podcasts", controllers.GetAllPodcasts)
r.GET("/podcasts/:id", controllers.GetPodcastById) r.GET("/podcasts/:id", controllers.GetPodcastById)
r.DELETE("/podcasts/:id", controllers.DeletePodcastById)
r.GET("/podcasts/:id/items", controllers.GetPodcastItemsByPodcastId) r.GET("/podcasts/:id/items", controllers.GetPodcastItemsByPodcastId)
r.GET("/podcastitems", controllers.GetAllPodcastItems) r.GET("/podcastitems", controllers.GetAllPodcastItems)
r.GET("/podcastitems/:id", controllers.GetPodcastItemById) r.GET("/podcastitems/:id", controllers.GetPodcastItemById)
r.GET("/", func(c *gin.Context) { r.GET("/add", controllers.AddPage)
//var podcasts []db.Podcast r.GET("/search", controllers.Search)
podcasts := service.GetAllPodcasts() r.GET("/", controllers.HomePage)
c.HTML(http.StatusOK, "index.html", gin.H{"title": "Podgrab", "podcasts": podcasts}) r.GET("/podcasts/:id/view", controllers.PodcastPage)
}) r.GET("/episodes", controllers.AllEpisodesPage)
r.GET("/podcasts/:id/view", func(c *gin.Context) {
var searchByIdQuery controllers.SearchByIdQuery
if c.ShouldBindUri(&searchByIdQuery) == nil {
var podcast db.Podcast
if err := db.GetPodcastById(searchByIdQuery.Id, &podcast); err == nil {
c.HTML(http.StatusOK, "podcast.html", gin.H{"title": podcast.Title, "podcast": podcast})
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
})
r.GET("/episodes", func(c *gin.Context) {
var pagination controllers.Pagination
if c.ShouldBindQuery(&pagination) == nil {
var page, count int
if page = pagination.Page; page == 0 {
page = 1
}
if count = pagination.Count; count == 0 {
count = 10
}
var podcastItems []db.PodcastItem
if err := db.GetPaginatedPodcastItems(page, count, &podcastItems); err == nil {
c.HTML(http.StatusOK, "episodes.html", gin.H{"title": "All Episodes", "podcastItems": podcastItems})
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
}
})
r.POST(
"/", func(c *gin.Context) {
var addPodcastData controllers.AddPodcastData
err := c.ShouldBind(&addPodcastData)
if err == nil {
_, err = service.AddPodcast(addPodcastData.Url)
if err == nil {
go service.RefreshEpisodes()
c.Redirect(http.StatusFound, "/")
} else {
c.JSON(http.StatusBadRequest, err)
}
} else {
// fmt.Println(err.Error())
c.JSON(http.StatusBadRequest, err)
}
})
go assetEnv() go assetEnv()
go intiCron() go intiCron()

@ -0,0 +1,11 @@
package model
import "fmt"
type PodcastAlreadyExistsError struct {
Url string
}
func (e *PodcastAlreadyExistsError) Error() string {
return fmt.Sprintf("Podcast with this url already exists")
}

@ -0,0 +1,21 @@
package model
type GPodcast struct {
URL string `json:"url"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
Subscribers int `json:"subscribers"`
SubscribersLastWeek int `json:"subscribers_last_week"`
LogoURL string `json:"logo_url"`
ScaledLogoURL string `json:"scaled_logo_url"`
Website string `json:"website"`
MygpoLink string `json:"mygpo_link"`
AlreadySaved bool `json:"already_saved"`
}
type GPodcastTag struct {
Tag string `json:"tag"`
Title string `json:"title"`
Usage int `json:"usage"`
}

@ -1,4 +1,4 @@
package service package model
import "encoding/xml" import "encoding/xml"

@ -1,6 +1,7 @@
package service package service
import ( import (
"errors"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -35,6 +36,15 @@ func Download(link string, episodeTitle string, podcastName string) (string, err
return finalPath, nil return finalPath, nil
} }
func DeleteFile(filePath string) error {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return errors.New("File does not exist")
}
if err := os.Remove(filePath); err != nil {
return err
}
return nil
}
func httpClient() *http.Client { func httpClient() *http.Client {
client := http.Client{ client := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error { CheckRedirect: func(r *http.Request, via []*http.Request) error {

@ -0,0 +1,47 @@
package service
import (
"encoding/json"
"fmt"
"net/url"
"github.com/akhilrex/podgrab/model"
)
// type GoodReadsService struct {
// }
const BASE = "https://gpodder.net"
func Query(q string) []model.GPodcast {
url := fmt.Sprintf("%s/search.json?q=%s", BASE, url.QueryEscape(q))
body, _ := makeQuery(url)
var response []model.GPodcast
json.Unmarshal(body, &response)
return response
}
func ByTag(tag string, count int) []model.GPodcast {
url := fmt.Sprintf("%s/api/2/tag/%s/%d.json", BASE, url.QueryEscape(tag), count)
body, _ := makeQuery(url)
var response []model.GPodcast
json.Unmarshal(body, &response)
return response
}
func Top(count int) []model.GPodcast {
url := fmt.Sprintf("%s/toplist/%d.json", BASE, count)
body, _ := makeQuery(url)
var response []model.GPodcast
json.Unmarshal(body, &response)
return response
}
func Tags(count int) []model.GPodcastTag {
url := fmt.Sprintf("%s/api/2/tags/%d.json", BASE, count)
body, _ := makeQuery(url)
var response []model.GPodcastTag
json.Unmarshal(body, &response)
return response
}

@ -10,17 +10,18 @@ import (
"time" "time"
"github.com/akhilrex/podgrab/db" "github.com/akhilrex/podgrab/db"
"github.com/microcosm-cc/bluemonday" "github.com/akhilrex/podgrab/model"
strip "github.com/grokify/html-strip-tags-go"
"gorm.io/gorm" "gorm.io/gorm"
) )
//FetchURL is //FetchURL is
func FetchURL(url string) (PodcastData, error) { func FetchURL(url string) (model.PodcastData, error) {
body, err := makeQuery(url) body, err := makeQuery(url)
if err != nil { if err != nil {
return PodcastData{}, err return model.PodcastData{}, err
} }
var response PodcastData var response model.PodcastData
err = xml.Unmarshal(body, &response) err = xml.Unmarshal(body, &response)
return response, err return response, err
} }
@ -30,21 +31,19 @@ func GetAllPodcasts() *[]db.Podcast {
return &podcasts return &podcasts
} }
func AddPodcast(url string) (db.Podcast, error) { func AddPodcast(url string) (db.Podcast, error) {
var podcast db.Podcast
err := db.GetPodcastByURL(url, &podcast)
if errors.Is(err, gorm.ErrRecordNotFound) {
data, err := FetchURL(url) data, err := FetchURL(url)
if err != nil { if err != nil {
fmt.Println("Error") fmt.Println("Error")
//log.Fatal(err) //log.Fatal(err)
return db.Podcast{}, err return db.Podcast{}, err
} }
var podcast db.Podcast
err = db.GetPodcastByTitleAndAuthor(data.Channel.Title, data.Channel.Author, &podcast)
if errors.Is(err, gorm.ErrRecordNotFound) {
p := bluemonday.StripTagsPolicy()
podcast := db.Podcast{ podcast := db.Podcast{
Title: data.Channel.Title, Title: data.Channel.Title,
Summary: p.Sanitize(data.Channel.Summary), Summary: strip.StripTags(data.Channel.Summary),
Author: data.Channel.Author, Author: data.Channel.Author,
Image: data.Channel.Image.URL, Image: data.Channel.Image.URL,
URL: url, URL: url,
@ -52,7 +51,7 @@ func AddPodcast(url string) (db.Podcast, error) {
err = db.CreatePodcast(&podcast) err = db.CreatePodcast(&podcast)
return podcast, err return podcast, err
} }
return podcast, err return podcast, &model.PodcastAlreadyExistsError{Url: url}
} }
@ -63,8 +62,12 @@ func AddPodcastItems(podcast *db.Podcast) error {
//log.Fatal(err) //log.Fatal(err)
return err return err
} }
p := bluemonday.StripTagsPolicy() //p := bluemonday.StrictPolicy()
for i := 0; i < 5; i++ { limit := 5
if len(data.Channel.Item) < limit {
limit = len(data.Channel.Item)
}
for i := 0; i < limit; i++ {
obj := data.Channel.Item[i] obj := data.Channel.Item[i]
var podcastItem db.PodcastItem var podcastItem db.PodcastItem
err := db.GetPodcastItemByPodcastIdAndGUID(podcast.ID, obj.Guid.Text, &podcastItem) err := db.GetPodcastItemByPodcastIdAndGUID(podcast.ID, obj.Guid.Text, &podcastItem)
@ -74,7 +77,7 @@ func AddPodcastItems(podcast *db.Podcast) error {
podcastItem = db.PodcastItem{ podcastItem = db.PodcastItem{
PodcastID: podcast.ID, PodcastID: podcast.ID,
Title: obj.Title, Title: obj.Title,
Summary: p.Sanitize(obj.Summary), Summary: strip.StripTags(obj.Summary),
EpisodeType: obj.EpisodeType, EpisodeType: obj.EpisodeType,
Duration: duration, Duration: duration,
PubDate: pubDate, PubDate: pubDate,
@ -128,10 +131,36 @@ func RefreshEpisodes() error {
return nil return nil
} }
func DeletePodcast(id string) error {
var podcast db.Podcast
err := db.GetPodcastById(id, &podcast)
if err != nil {
return err
}
var podcastItems []db.PodcastItem
err = db.GetAllPodcastItemsByPodcastId(id, &podcastItems)
if err != nil {
return err
}
for _, item := range podcastItems {
DeleteFile(item.DownloadPath)
db.DeletePodcastItemById(item.ID)
}
err = db.DeletePodcastById(id)
if err != nil {
return err
}
return nil
}
func makeQuery(url string) ([]byte, error) { func makeQuery(url string) ([]byte, error) {
//link := "https://www.goodreads.com/search/index.xml?q=Good%27s+Omens&key=" + "jCmNlIXjz29GoB8wYsrd0w" //link := "https://www.goodreads.com/search/index.xml?q=Good%27s+Omens&key=" + "jCmNlIXjz29GoB8wYsrd0w"
//link := "https://www.goodreads.com/search/index.xml?key=jCmNlIXjz29GoB8wYsrd0w&q=Ender%27s+Game" //link := "https://www.goodreads.com/search/index.xml?key=jCmNlIXjz29GoB8wYsrd0w&q=Ender%27s+Game"
//fmt.Println(url) fmt.Println(url)
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return nil, err return nil, err

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save