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}}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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"
|
||||||
|
|
@ -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
|
||||||
|
}
|
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…
Reference in new issue