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">
<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>
{{template "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; }
img{
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{
margin-bottom: 1rem;
}
@ -167,9 +55,9 @@ hr{
<h4>{{.Title}} // {{ .Podcast.Title}}</h4>
<small>{{ formatDate .PubDate }}</small>
<p>{{ .Summary }}</p>
{{if .DownloadPath}}
<a class="button button-primary" href="{{ .DownloadPath }}" download>Download</a>
{{end }}
</div>
<div class="columns one">

@ -5,124 +5,12 @@
<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>PodGrab</title>
{{template "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: 18rem;
.button-delete{
background-color: indianred;
color:wheat;
}
.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>
</head>
<body>
@ -131,7 +19,7 @@
<h1>{{ .title }}</h1>
</section>
{{template "navbar"}}
<div class="row">
<div class="row" style="display: none;">
<form action="/" method="post">
<div class="nine columns">
<input type="url" name="url" id="url" placeholder="Enter Podcast RSS feed to add" class="u-full-width">
@ -141,29 +29,77 @@
</form>
</div>
<br>
{{range .podcasts}}
<div class="podcasts row">
<div class="columns three">
<div class="podcasts row" id="podcast-{{ .ID }}">
<div class="columns two">
<img class="u-full-width" src="{{ .Image }}" alt="{{ .Title }}">
</div>
<div class="columns eight">
<h3>{{.Title}}</h3>
<small>Last Epidsode : {{ latestEpisodeDate .PodcastItems}}</small>
<div class="columns ten">
<a style="text-decoration: none;" href="/podcasts/{{ .ID }}/view"> <h3>{{.Title}}</h3></a>
<p>{{ .Summary }}</p>
<div class="row">
<div class="columns four">
Last Episode : {{ latestEpisodeDate .PodcastItems}}
</div>
<div class="columns one">
<a href="/podcasts/{{ .ID }}/view"> {{ len .PodcastItems }} episodes</a>
</div>
<div class="columns four">
{{ len .PodcastItems }} episodes
</div>
<div class="columns four">
<button class="button button-delete" onclick="deletePodcast('{{.ID}}')">Delete</button>
</div>
</div>
</div>
</div>
<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}}
<div class="row">
<!-- <div class="row">
<div class="columns twelve" style="text-align: center;">
<a href="/episodes" class="button button-primary">All Episodes</a>
</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>
</html>

@ -51,6 +51,7 @@
<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="/episodes">Episodes</a></li>
<li class="navbar-item"><a class="navbar-link" href="/add">Add Podcast</a></li>
</ul>
</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 (
"fmt"
"log"
"net/http"
"github.com/akhilrex/podgrab/model"
"github.com/akhilrex/podgrab/service"
"github.com/akhilrex/podgrab/db"
@ -48,6 +50,17 @@ func GetPodcastById(c *gin.Context) {
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) {
var searchByIdQuery SearchByIdQuery
@ -89,12 +102,20 @@ func AddPodcast(c *gin.Context) {
var addPodcastData AddPodcastData
err := c.ShouldBindJSON(&addPodcastData)
if err == nil {
service.AddPodcast(addPodcastData.Url)
// fmt.Println(time.Unix(addPodcastData.StartDate, 0))
c.JSON(200, addPodcastData)
pod, err := service.AddPodcast(addPodcastData.Url)
if err == nil {
go service.RefreshEpisodes()
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 {
fmt.Println(err.Error())
c.JSON(http.StatusBadRequest, err)
log.Println(err.Error())
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)
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 {
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)
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 {

@ -6,6 +6,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
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/joho/godotenv v1.3.0
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/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
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/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=

@ -4,7 +4,6 @@ import (
"fmt"
"html/template"
"log"
"net/http"
"os"
"strconv"
"time"
@ -30,7 +29,7 @@ func main() {
}
r := gin.Default()
dataPath := os.Getenv("DATA")
//r.Static("/assets", "./assets")
r.Static("/webassets", "./webassets")
r.Static("/assets", dataPath)
funcMap := template.FuncMap{
"formatDate": func(raw time.Time) string {
@ -51,80 +50,20 @@ func main() {
//r.LoadHTMLGlob("client/*")
r.SetHTMLTemplate(tmpl)
r.GET("/podcasts", controllers.AddPodcast)
r.POST("/podcasts", controllers.GetAllPodcasts)
r.POST("/podcasts", controllers.AddPodcast)
r.GET("/podcasts", controllers.GetAllPodcasts)
r.GET("/podcasts/:id", controllers.GetPodcastById)
r.DELETE("/podcasts/:id", controllers.DeletePodcastById)
r.GET("/podcasts/:id/items", controllers.GetPodcastItemsByPodcastId)
r.GET("/podcastitems", controllers.GetAllPodcastItems)
r.GET("/podcastitems/:id", controllers.GetPodcastItemById)
r.GET("/", func(c *gin.Context) {
//var podcasts []db.Podcast
podcasts := service.GetAllPodcasts()
c.HTML(http.StatusOK, "index.html", gin.H{"title": "Podgrab", "podcasts": podcasts})
})
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)
}
})
r.GET("/add", controllers.AddPage)
r.GET("/search", controllers.Search)
r.GET("/", controllers.HomePage)
r.GET("/podcasts/:id/view", controllers.PodcastPage)
r.GET("/episodes", controllers.AllEpisodesPage)
go assetEnv()
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"

@ -1,6 +1,7 @@
package service
import (
"errors"
"io"
"net/http"
"net/url"
@ -35,6 +36,15 @@ func Download(link string, episodeTitle string, podcastName string) (string, err
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 {
client := http.Client{
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"
"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"
)
//FetchURL is
func FetchURL(url string) (PodcastData, error) {
func FetchURL(url string) (model.PodcastData, error) {
body, err := makeQuery(url)
if err != nil {
return PodcastData{}, err
return model.PodcastData{}, err
}
var response PodcastData
var response model.PodcastData
err = xml.Unmarshal(body, &response)
return response, err
}
@ -30,21 +31,19 @@ func GetAllPodcasts() *[]db.Podcast {
return &podcasts
}
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)
if err != nil {
fmt.Println("Error")
//log.Fatal(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{
Title: data.Channel.Title,
Summary: p.Sanitize(data.Channel.Summary),
Summary: strip.StripTags(data.Channel.Summary),
Author: data.Channel.Author,
Image: data.Channel.Image.URL,
URL: url,
@ -52,7 +51,7 @@ func AddPodcast(url string) (db.Podcast, error) {
err = db.CreatePodcast(&podcast)
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)
return err
}
p := bluemonday.StripTagsPolicy()
for i := 0; i < 5; i++ {
//p := bluemonday.StrictPolicy()
limit := 5
if len(data.Channel.Item) < limit {
limit = len(data.Channel.Item)
}
for i := 0; i < limit; i++ {
obj := data.Channel.Item[i]
var podcastItem db.PodcastItem
err := db.GetPodcastItemByPodcastIdAndGUID(podcast.ID, obj.Guid.Text, &podcastItem)
@ -74,7 +77,7 @@ func AddPodcastItems(podcast *db.Podcast) error {
podcastItem = db.PodcastItem{
PodcastID: podcast.ID,
Title: obj.Title,
Summary: p.Sanitize(obj.Summary),
Summary: strip.StripTags(obj.Summary),
EpisodeType: obj.EpisodeType,
Duration: duration,
PubDate: pubDate,
@ -128,10 +131,36 @@ func RefreshEpisodes() error {
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) {
//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"
//fmt.Println(url)
fmt.Println(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
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