Merge pull request #67 from akhilrex/podcasts_page

pull/72/head
Akhil Gupta 4 years ago committed by GitHub
commit 8372faebf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,5 +3,10 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
"configurations": [{
"program": "${workspaceRoot}/main.go",
"type": "go",
"mode": "auto"
}
]
}

@ -188,41 +188,41 @@
<div class="row">
<div class="columns twelve" style="text-align: center">
{{if .previousPage }}
{{if .filter.PreviousPage }}
<a
href="?page=1&downloadedOnly={{.downloadedOnly}}"
class="button"
>Newest</a
>First</a
>
{{end}}
{{if .previousPage }}
{{if .filter.PreviousPage }}
<a
href="?page={{.previousPage}}&downloadedOnly={{.downloadedOnly}}"
href="?page={{.filter.PreviousPage}}&downloadedOnly={{.downloadedOnly}}"
class="button"
>Newer</a
>Last</a
>
{{end}}
<select name="page" id="pageDdl">
{{ $page:=.page }}
{{range $y := intRange 1 .totalPages}}
{{range $y := intRange 1 .filter.TotalPages}}
<option {{if eq $page $y }} selected="selected" {{end}}}>{{$y}}</option>
{{end}}
</select>
{{if .nextPage }}
{{if .filter.NextPage }}
<a
href="?page={{.nextPage}}&downloadedOnly={{.downloadedOnly}}"
href="?page={{.filter.NextPage}}&downloadedOnly={{.downloadedOnly}}"
class="button"
>Older</a
>Next</a
>
{{end}}
{{if gt .totalPages .page }}
{{if gt .filter.TotalPages .page }}
<a
href="?page={{.totalPages}}&downloadedOnly={{.downloadedOnly}}"
href="?page={{.filter.TotalPages}}&downloadedOnly={{.downloadedOnly}}"
class="button"
>Oldest</a
>Last</a
>
{{end}}
</div>

@ -0,0 +1,737 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.title}} - PodGrab</title>
{{template "commoncss" .}}
<link rel="stylesheet" href="/webassets/vue-multiselect.min.css">
<style>
img {
display: none;
}
h2,
h3,
h4,
h5 {
margin-bottom: 1rem;
}
h4 {
font-size: 2rem;
}
h5 {
font-size: 1.5rem;
}
p {
margin-bottom: 0.5rem;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
}
.podcastItem .button {
padding: 0 15px;
}
.IsPlayed-true {
color: #555555;
}
/* Larger than tablet */
@media (min-width: 750px) {
img {
display: block;
}
}
.button-enqueue{
display: none;
}
body.playerExists .button-enqueue{
display: inline-block;
[v-cloak] {
display: none;
}
}
</style>
</head>
<body>
<div class="container">
{{template "navbar" .}}
<br />{{$setting := .setting}}
<div id="app" v-cloak>
<button @click="toggleFilters()" class="u-full-width">
<span v-show="!showFilters">Show Filters</span>
<span v-show="showFilters">Hide Filters</span>
</button>
<form @submit.prevent="submitFilters()" v-show="showFilters">
<div class="row">
<input class="columns five" type="search" @input="searchQueryUpated()" v-model="filter.q" placeholder="Search">
<div class="columns three"> <vue-multiselect v-model="selectedSorting" :options="sortOptions" :searchable="false"
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
</vue-multiselect></div>
<div class="columns one"> <vue-multiselect :show-pointer="false" v-model="filter.count" :options="pagingOptions" :searchable="false" :allow-empty="false" :close-on-select="true" :show-labels="false" placeholder="Pick a value"></vue-multiselect>
</div>
<div class="columns three"> <vue-multiselect v-model="selectedPlayedStatus" :options="playedStatusOptions" :searchable="false"
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
</vue-multiselect></div>
</div>
<div class="row">
<vue-multiselect class="columns three" v-model="selectedPodcasts" :options="podcasts" :show-labels="false"
:multiple="true" :close-on-select="false" :clear-on-select="false"
:preserve-search="true" placeholder="Podcasts" label="Title" track-by="ID" :preselect-first="false">
<template slot="selection" slot-scope="{ values, search, isOpen }">
<span class="multiselect__single" v-if="values.length && !isOpen">${ values.length } options selected</span>
</template>
</vue-multiselect>
<vue-multiselect class="columns three" v-model="selectedTags" :show-labels="false" :options="tags" :multiple="true" :close-on-select="false" :clear-on-select="false" :preserve-search="true" placeholder="Tags" label="Label" track-by="ID" :preselect-first="false">
<template slot="selection" slot-scope="{ values, search, isOpen }"><span class="multiselect__single" v-if="values.length && !isOpen">${ values.length } options selected</span></template>
</vue-multiselect>
<div class="columns three"> <vue-multiselect v-model="selectedDownloadStatus" :options="downloadStatusOptions" :searchable="false"
:multiple="false" :close-on-select="true" :clear-on-select="true" :allow-empty="false" :show-labels="false"
placeholder="Sort By" label="Label" track-by="Value" :preselect-first="true">
</vue-multiselect></div>
<div class="columns three">
<a title="Play items in this page" v-if="podcastItems.length" class="button" @click="playPage()"><i class="fas fa-play"></i></a>
<a title="Enqueue items in this page" v-if="podcastItems.length" class="button button-enqueue" @click="enqueuePage()"><i class="fas fa-plus"></i></a>
<a title="Reset Filters" class="button" @click="resetFilters()"><i class="fas fa-undo"></i></a>
</div>
</div>
</form>
<hr>
<template v-for="item in podcastItems" >
<div class="podcasts row podcastItem" >
<div class="columns two">
<img
onerror="onImageError(this)"
class="u-full-width"
:src=" getEpisodeImage(item)"
v-bind:alt="item.Title"
lazy="lazy"
/>
</div>
<div class="columns ten">
<div class="row">
<div class="columns eight">
<h4>
<i
v-if="item.IsPlayed"
title="Played"
style="color: green"
class="fas fa-check-circle"
></i>
${item.Title} <template v-if="item.Podcast && item.Podcast.Title"> // ${item.Podcast.Title}</template>
</h4>
</div>
<div class="columns three">
<small :title="item.PubDate">${getRelativeDate(item.PubDate)}</small
>
</div>
<div class="columns one">
<small> ${getFormattedDuration(item.Duration)}</small>
</div>
</div>
<p class="useMore">${item.Summary }</p>
<a
v-if="item.IsPlayed"
class="button button"
title="Mark as not listened"
@click="changePlayedStatus(item)"
><i class="fas fa-envelope"></i
></a>
<a
v-if="!item.IsPlayed"
class="button button"
title="Mark as listened"
@click="changePlayedStatus(item)"
><i class="fas fa-envelope-open"></i
></a>
<a
v-if="!isBookmarked(item)"
class="button button"
title="Bookmark Episode"
@click="changeBookmarkStatus(item)"
><i class="far fa-bookmark"></i
></a>
<a
v-if="isBookmarked(item)"
class="button button"
title="Remove Bookmark"
@click="changeBookmarkStatus(item)"
><i class="fas fa-bookmark"></i
></a>
<a
v-if="item.DownloadPath"
class="button"
:href="removeStartingSlash(item.DownloadPath)"
download
title="Download episode file"
><i class="fas fa-download"></i
></a>
<a
v-if="item.DownloadPath"
class="button button"
@click="deleteFile(item)"
title="Delete Podcast Episode File"
><i class="fas fa-trash"></i
></a>
<a
v-if="settings.AutoDownload && !item.DownloadPath"
class="button button"
@click="downloadToDisk(item)"
title="Download to server"
download
><i class="fas fa-cloud-download-alt"></i
></a>
<a
v-if="!settings.AutoDownload && !item.DownloadPath && item.DownloadStatus===3"
class="button button"
@click="downloadToDisk(item)"
title="Download to server"
download
><i class="fas fa-cloud-download-alt"></i
></a>
<a
class="button button"
@click="openPlayer(item)"
title="Play Episode"
><i class="fas fa-play"></i
></a>
<a
class="button button-enqueue"
@click="enqueueEpisode(item)"
title="Add Episode to existing player playlist"
><i class="fas fa-plus"></i
></a>
</div> <div class="columns one"></div>
</div>
<hr />
</template>
<p v-if="podcastItems.length===0">Nothing to see here! Maybe try changing the filter criteria.</p>
<div class="row" v-if="podcastItems.length">
<div class="columns twelve" style="text-align: center">
<a v-if="filter.previousPage"
@click=" goToPage(1)"
class="button"
>First</a
>
<a v-if="filter.previousPage"
@click="goToPreviousPage()"
class="button"
>Previous</a
>
<select name="page" id="pageDdl" v-model="filter.page" >
<option :key="index" v-for="index in filter.totalPages" >${index}</option>
</select>
<a
v-if="filter.nextPage"
class="button"
@click="goToNextPage()"
>Next</a
>
<a
v-if="filter.totalPages>filter.page"
class="button"
@click=" goToPage(filter.totalPages)"
>Last</a
>
</div>
</div>
</div>
{{template "scripts"}}
<script src="/webassets/luxon.min.js"></script>
<script src="/webassets/vue-multiselect.min.js"></script>
<script>
Vue.component('vue-multiselect', window.VueMultiselect.default)
var app = new Vue({
delimiters: ["${", "}"],
el: "#app",
computed:{
page(){
return this.filter.page;
},
count(){
return this.filter.count;
}
},
watch:{
page(newPage,oldPage){
if(newPage==oldPage){
return;
}
if(newPage==1){
this.filter.previousPage=0;
}else{
this.filter.previousPage=newPage-1;
}
if(newPage==this.filter.totalPages){
this.filter.nextPage=0;
}else{
this.filter.nextPage=newPage+1;
}
this.getData()
},
count(current,old){
this.getData()
},
showFilters(current,old){
if(localStorage){
localStorage.showFilters=current
}
},
selectedPodcasts(current,old){
var arr=[];
for (let index = 0; index < current.length; index++) {
const element = current[index];
arr.push(element.ID);
}
this.filter['podcastIds']=arr;
this.submitFilters()
},
selectedTags(current,old){
var arr=[];
for (let index = 0; index < current.length; index++) {
const element = current[index];
arr.push(element.ID);
}
this.filter['tagIds']=arr;
this.submitFilters()
},
selectedSorting(current,old){
this.filter.sorting=current.Value;
this.submitFilters()
},
selectedDownloadStatus(current,old){
this.filter.isDownloaded=current.Value;
this.submitFilters()
},
selectedPlayedStatus(current,old){
this.filter.isPlayed=current.Value;
this.submitFilters()
},
},
mounted(){
if(localStorage && localStorage.episodesFilter){
this.filter=JSON.parse(localStorage.episodesFilter);
}
if(localStorage && localStorage.showFilters){
this.showFilters=JSON.parse(localStorage.showFilters);
}
if(this.filter.podcastIds!=null){
var selectedPodcasts=[];
for(var i=0;i<this.podcasts.length;i++){
for(var j=0;j<this.filter.podcastIds.length;j++){
if(this.filter.podcastIds[j]===this.podcasts[i].ID){
selectedPodcasts.push( this.podcasts[i])
}
}
}
this.selectedPodcasts=selectedPodcasts
}
if(this.filter.tagIds!=null){
var selectedTags=[];
for(var i=0;i<this.tags.length;i++){
for(var j=0;j<this.filter.tagIds.length;j++){
if(this.filter.tagIds[j]===this.tags[i].ID){
selectedTags.push( this.tags[i])
}
}
}
this.selectedTags=selectedTags
}
for(var i=0;i<this.sortOptions.length;i++){
if(this.sortOptions[i].Value===this.filter.sorting){
this.selectedSorting=this.sortOptions[i]
}
}
for(var i=0;i<this.downloadStatusOptions.length;i++){
if(this.downloadStatusOptions[i].Value===this.filter.isDownloaded.toString()){
this.selectedDownloadStatus=this.downloadStatusOptions[i]
}
}
for(var i=0;i<this.playedStatusOptions.length;i++){
if(this.playedStatusOptions[i].Value===this.filter.isPlayed.toString()){
this.selectedPlayedStatus=this.playedStatusOptions[i]
}
}
this.getData()
},
methods:{
getRelativeDate(dt){
return luxon.DateTime.fromISO(dt).toRelative()
},
getFormattedDuration(duration){
var obj=luxon.Duration.fromObject({seconds:duration}).shiftTo("hours","minutes","seconds").toObject();
str="";
if(obj.hours>0){
str+=obj.hours+":"
}
str+=obj.minutes+":"+obj.seconds;
return str;
},
getEpisodeImage(item){
return "/podcastitems/"+item.ID+"/image"
},
changePlayedStatus(item){
changePlayedStatus(item.ID,!item.IsPlayed,()=>item.IsPlayed=!item.IsPlayed)
},
goToPreviousPage(pageNumber){
this.filter.page=this.filter.previousPage;
},
goToNextPage(pageNumber){
this.filter.page=this.filter.nextPage;
},
goToPage(pageNumber){
this.filter.page=pageNumber;
},
changeBookmarkStatus(item){
isBookmarked= this.isBookmarked(item);
let self=this;
changeBookmarkStatus(item.ID,!isBookmarked,function(){
if(isBookmarked){
item.BookmarkDate=self.nildate
}else{
item.BookmarkDate="new Date().toDateString()"
}
})
},
isBookmarked(item){
return item.BookmarkDate!==this.nildate
},
deleteFile(item){
deleteFile(item.ID)
},
downloadToDisk(item){
downloadToDisk(item.ID)
},
openPlayer(item){
openPlayer([item.ID])
},
enqueueEpisode(item){
enqueueEpisode([item.ID])
},
searchQueryUpated(){
var self=this;
clearTimeout(this.debounce)
this.debounce = setTimeout(() => {
self.filter.page=1
self.getData()
}, 600)
},
saveFilter(data){
if(localStorage){
localStorage.episodesFilter=JSON.stringify(data);
}
},
submitFilters(){
this.filter.page=1;
this.getData();
},
toggleFilters(){
this.showFilters=!this.showFilters
},
playPage(){
var itemIds=[];
for(var i=0;i<this.podcastItems.length;i++){
itemIds.push(this.podcastItems[i].ID)
}
openPlayer(itemIds)
},
enqueuePage(){
var itemIds=[];
for(var i=0;i<this.podcastItems.length;i++){
itemIds.push(this.podcastItems[i].ID)
}
enqueueEpisode(itemIds)
},
updateUrl(){
var url= new URL(window.top.location.href);
var copy= Vue.util.extend({},this.filter);
delete copy.totalPages
delete copy.totalCount
delete copy.nextPage
delete copy.previousPage
url.search=new URLSearchParams(copy);
history.pushState(null,null,url.href)
},
resetFilters(){
this.filter.q="";
this.selectedPodcasts=[];
this.selectedTags=[];
this.selectedDownloadStatus=this.downloadStatusOptions[0];
this.selectedPlayedStatus=this.playedStatusOptions[0];
},
removeStartingSlash(url){
if(url[0]==='/'){
return url
}
return "/"+url
},
getData(){
var self=this;
axios
.get("/podcastitems",{params:this.filter})
.then(function (response) {
self.podcastItems= response.data.podcastItems;
self.filter=response.data.filter;
self.saveFilter(self.filter);
self.updateUrl();
setPageTitle("Episodes ("+self.filter.totalCount+")")
})
.catch(function (error) {
if (error.response && error.response.data && error.response.data.message) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
}
},
data: {
socket:null,
debouce:null,
nildate:"0001-01-01T00:00:00Z",
playerExists:false,
isMobile:false,
sortOrder:"dateAdded-asc",
selectedSorting:"release_desc",
selectedPodcasts:[],
selectedTags:[],
selectedDownloadStatus:"",
selectedPlayedStatus:"",
showFilters:localStorage && JSON.parse(localStorage.showFilters),
{{ $len := len .podcastItems}}
podcastItems: {{if gt $len 0}} {{ .podcastItems }} {{else}} [] {{end}},
settings:{{.setting}},
filter:{{.filter}},
podcasts:{{.podcasts}},
tags:{{.tags}},
sortOptions:{{.sortOptions}},
pagingOptions:[10,20,50,100],
downloadStatusOptions:[{"Label":"All","Value":"nil"},{"Label":"Downloaded Only","Value":"true"},{"Label":"Not Downloaded","Value":"false"}],
playedStatusOptions:[{"Label":"All","Value":"nil"},{"Label":"Played Only","Value":"true"},{"Label":"Unplayed only","Value":"false"}],
}})
</script>
<script>
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
function downloadToDisk(id) {
axios
.get("/podcastitems/" + id + "/download")
.then(function (response) {
Vue.toasted.show("Podcast download enqueued.", {
theme: "bubble",
type: "info",
position: "top-right",
duration: 5000,
});
var row = document.getElementById("podcast-" + id);
row.remove();
})
.catch(function (error) {
if (error.response && error.response.data && error.response.data.message) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
return false;
}
function deleteFile(id) {
axios
.get("/podcastitems/" + id + "/delete")
.then(function (response) {
Vue.toasted.show("Podcast file deleted.", {
theme: "bubble",
type: "success",
position: "top-right",
duration: 5000,
});
var row = document.getElementById("podcast-" + id);
row.remove();
})
.catch(function (error) {
if (error.response && error.response.data && error.response.data.message) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
return false;
}
function changePlayedStatus(id, status,success) {
var endpoint = status ? "markPlayed" : "markUnplayed";
axios
.get("/podcastitems/" + id + "/" + endpoint, {
isPlayed: status,
})
.then(function (response) {
Vue.toasted.show("Podcast played status updated.", {
theme: "bubble",
type: "info",
position: "top-right",
duration: 5000,
});
if(success){
success();
}
})
.catch(function (error) {
if (error.response && error.response.data && error.response.data.message) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
return false;
}
function changeBookmarkStatus(id, status,success) {
var endpoint = status ? "bookmark" : "unbookmark";
axios
.get("/podcastitems/" + id + "/" + endpoint, {
isPlayed: status,
})
.then(function (response) {
msg= status?"Bookmark Added": "Bookmark removed";
Vue.toasted.show(msg, {
theme: "bubble",
type: "info",
position: "top-right",
duration: 5000,
});
if(success){
success()
}
})
.catch(function (error) {
if (error.response && error.response.data && error.response.data.message) {
Vue.toasted.show(error.response.data.message, {
theme: "bubble",
type: "error",
position: "top-right",
duration: 5000,
});
}
})
.then(function () {});
return false;
}
</script>
<script>
const socket= getWebsocketConnection(function(event){
const message= getWebsocketMessage("Register","Home")
socket.send(message);
},function(x){
const msg= JSON.parse(x.data)
if(msg.messageType=="NoPlayer"){
document.body.classList.remove("playerExists")
}
if(msg.messageType=="PlayerExists"){
document.body.classList.add("playerExists")
}
});
function enqueueEpisode(ids){
if(!socket){
return
}
socket.send(getWebsocketMessage("Enqueue",`{"itemIds":${JSON.stringify(ids)}}`))
}
function enquePodcast(id){
if(!socket){
return
}
socket.send(getWebsocketMessage("Enqueue",`{"podcastId":"${id}"}`))
}
function playPodcast(id){
openPlayer("",id)
}
</script>
</body>
</html>

@ -141,7 +141,7 @@ div#overlay{
<div></div>
<div></div>
</a>
<h1>{{ .title }}</h1>
<h1 id="pageTitle">{{ .title }}</h1>
{{if .podcastId }}
<button
class="button"
@ -199,6 +199,11 @@ onclick="enquePodcast({{ .podcastId }})"
<div id="overlay" onclick="toggleMenu()"></div>
<script>
function setPageTitle(content){
document.getElementById('pageTitle').innerText=content;
}
function toggleMenu(){
var sideDrawer= document.getElementById('sideDrawer')
var overlay= document.getElementById('overlay')

@ -1,5 +1,5 @@
{{define "scripts"}}
<script src="/webassets/vue.min.js"></script>
<script src="/webassets/vue.js"></script>
<script src="/webassets/axios.min.js"></script>
<script src="/webassets/vue-toasted.min.js"></script>
@ -79,10 +79,12 @@
}
checkUseMore();
function openPlayer(itemId, podcastId,tagIds) {
function openPlayer(itemIds, podcastId,tagIds) {
var url = "/player?";
if (itemId) {
url += "&itemId=" + itemId;
if(itemIds && itemIds.length>0){
for (const itemId of itemIds) {
url += "&itemIds=" + itemId;
}
}
if (podcastId) {
url += "&podcastId=" + podcastId;

@ -11,6 +11,7 @@ import (
"time"
"github.com/akhilrex/podgrab/db"
"github.com/akhilrex/podgrab/model"
"github.com/akhilrex/podgrab/service"
"github.com/gin-gonic/gin"
)
@ -55,7 +56,7 @@ func PodcastPage(c *gin.Context) {
var podcast db.Podcast
if err := db.GetPodcastById(searchByIdQuery.Id, &podcast); err == nil {
var pagination Pagination
var pagination model.Pagination
if c.ShouldBindQuery(&pagination) == nil {
var page, count int
if page = pagination.Page; page == 0 {
@ -94,7 +95,7 @@ func PodcastPage(c *gin.Context) {
"podcastId": searchByIdQuery.Id,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
c.JSON(http.StatusBadRequest, err)
}
} else {
c.JSON(http.StatusBadRequest, err)
@ -105,11 +106,11 @@ func PodcastPage(c *gin.Context) {
}
func getItemsToPlay(itemId, podcastId string, tagIds []string) []db.PodcastItem {
func getItemsToPlay(itemIds []string, podcastId string, tagIds []string) []db.PodcastItem {
var items []db.PodcastItem
if itemId != "" {
toAdd := service.GetPodcastItemById(itemId)
items = append(items, *toAdd)
if len(itemIds) > 0 {
toAdd, _ := service.GetAllPodcastItemsByIds(itemIds)
items = *toAdd
} else if podcastId != "" {
pod := service.GetPodcastById(podcastId)
@ -131,16 +132,16 @@ func getItemsToPlay(itemId, podcastId string, tagIds []string) []db.PodcastItem
func PlayerPage(c *gin.Context) {
itemId, hasItemId := c.GetQuery("itemId")
itemIds, hasItemIds := c.GetQueryArray("itemIds")
podcastId, hasPodcastId := c.GetQuery("podcastId")
tagIds, hasTagIds := c.GetQueryArray("tagIds")
title := "Podgrab"
var items []db.PodcastItem
var totalCount int64
if hasItemId {
toAdd := service.GetPodcastItemById(itemId)
items = append(items, *toAdd)
totalCount = 1
if hasItemIds {
toAdd, _ := service.GetAllPodcastItemsByIds(itemIds)
items = *toAdd
totalCount = int64(len(items))
} else if hasPodcastId {
pod := service.GetPodcastById(podcastId)
items = pod.PodcastItems
@ -224,73 +225,41 @@ func BackupsPage(c *gin.Context) {
}
}
func AllEpisodesPage(c *gin.Context) {
var pagination Pagination
var page, count int
c.ShouldBindQuery(&pagination)
if page = pagination.Page; page == 0 {
page = 1
}
if count = pagination.Count; count == 0 {
count = 10
}
var filter EpisodesFilter
c.ShouldBindQuery(&filter)
var podcastItems []db.PodcastItem
var totalCount int64
//fmt.Printf("%+v\n", filter)
fromDate := time.Time{}
if filter.FromDate != "" {
parsedDate, err := time.Parse("2006-01-02", strings.Trim(filter.FromDate, "\""))
if err != nil {
fromDate = time.Time{}
} else {
fromDate = parsedDate
func getSortOptions() interface{} {
return []struct {
Label, Value string
}{
{"Release (asc)", "release_asc"},
{"Release (desc)", "release_desc"},
{"Duration (asc)", "duration_asc"},
{"Duration (desc)", "duration_desc"},
}
}
if err := db.GetPaginatedPodcastItems(page, count,
nil, filter.PlayedOnly, fromDate,
&podcastItems, &totalCount); err == nil {
func AllEpisodesPage(c *gin.Context) {
var filter model.EpisodesFilter
c.ShouldBindQuery(&filter)
filter.VerifyPaginationValues()
setting := c.MustGet("setting").(*db.Setting)
totalPages := int(math.Ceil(float64(totalCount) / float64(count)))
nextPage, previousPage := 0, 0
if page < totalPages {
nextPage = page + 1
}
if page > 1 {
previousPage = page - 1
}
downloadedOnly := false
if filter.DownloadedOnly != nil {
downloadedOnly = *filter.DownloadedOnly
fmt.Println(downloadedOnly)
}
podcasts := service.GetAllPodcasts("")
tags, _ := db.GetAllTags("")
toReturn := gin.H{
"title": "All Episodes",
"podcastItems": podcastItems,
"podcastItems": []db.PodcastItem{},
"setting": setting,
"page": page,
"count": count,
"totalCount": totalCount,
"totalPages": totalPages,
"nextPage": nextPage,
"previousPage": previousPage,
"downloadedOnly": downloadedOnly,
}
fmt.Printf("%+v\n", totalCount)
c.HTML(http.StatusOK, "episodes.html", toReturn)
} else {
c.JSON(http.StatusBadRequest, err)
"page": filter.Page,
"count": filter.Count,
"filter": filter,
"podcasts": podcasts,
"tags": tags,
"sortOptions": getSortOptions(),
}
c.HTML(http.StatusOK, "episodes_new.html", toReturn)
}
func AllTagsPage(c *gin.Context) {
var pagination Pagination
var pagination model.Pagination
var page, count int
c.ShouldBindQuery(&pagination)
if page = pagination.Page; page == 0 {

@ -45,17 +45,6 @@ type AddRemoveTagQuery struct {
TagId string `binding:"required" uri:"tagId" json:"tagId" form:"tagId"`
}
type Pagination struct {
Page int `uri:"page" query:"page" json:"page" form:"page"`
Count int `uri:"count" query:"count" json:"count" form:"count"`
}
type EpisodesFilter struct {
DownloadedOnly *bool `uri:"downloadedOnly" query:"downloadedOnly" json:"downloadedOnly" form:"downloadedOnly"`
PlayedOnly *bool `uri:"playedOnly" query:"playedOnly" json:"playedOnly" form:"playedOnly"`
FromDate string `uri:"fromDate" query:"fromDate" json:"fromDate" form:"fromDate"`
}
type PatchPodcastItem struct {
IsPlayed bool `json:"isPlayed" form:"isPlayed" query:"isPlayed"`
Title string `form:"title" json:"title" query:"title"`
@ -182,9 +171,23 @@ func DownloadAllEpisodesByPodcastId(c *gin.Context) {
}
func GetAllPodcastItems(c *gin.Context) {
var podcasts []db.PodcastItem
db.GetAllPodcastItems(&podcasts)
c.JSON(200, podcasts)
var filter model.EpisodesFilter
err := c.ShouldBindQuery(&filter)
if err != nil {
fmt.Println(err.Error())
}
filter.VerifyPaginationValues()
if podcastItems, totalCount, err := db.GetPaginatedPodcastItemsNew(filter); err == nil {
filter.SetCounts(totalCount)
toReturn := gin.H{
"podcastItems": podcastItems,
"filter": &filter,
}
c.JSON(http.StatusOK, toReturn)
} else {
c.JSON(http.StatusBadRequest, err)
}
}
func GetPodcastItemById(c *gin.Context) {

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

@ -4,8 +4,11 @@ import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/akhilrex/podgrab/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@ -35,6 +38,63 @@ func GetAllPodcastItemsWithoutSize() (*[]PodcastItem, error) {
result := DB.Where("file_size<=?", 0).Order("pub_date desc").Find(&podcasts)
return &podcasts, result.Error
}
func getSortOrder(sorting model.EpisodeSort) string {
switch sorting {
case model.RELEASE_ASC:
return "pub_date asc"
case model.RELEASE_DESC:
return "pub_date desc"
case model.DURATION_ASC:
return "duration asc"
case model.DURATION_DESC:
return "duration desc"
default:
return "pub_date desc"
}
}
func GetPaginatedPodcastItemsNew(queryModel model.EpisodesFilter) (*[]PodcastItem, int64, error) {
var podcasts []PodcastItem
var total int64
query := DB.Debug().Preload("Podcast")
if queryModel.IsDownloaded != nil {
isDownloaded, err := strconv.ParseBool(*queryModel.IsDownloaded)
if err == nil && isDownloaded {
query = query.Where("download_status=?", Downloaded)
} else {
query = query.Where("download_status!=?", Downloaded)
}
}
if queryModel.IsPlayed != nil {
isPlayed, err := strconv.ParseBool(*queryModel.IsPlayed)
if err == nil && isPlayed {
query = query.Where("is_played=?", 1)
} else {
query = query.Where("is_played=?", 0)
}
}
if queryModel.Q != "" {
query = query.Where("UPPER(title) like ?", "%"+strings.TrimSpace(strings.ToUpper(queryModel.Q))+"%")
}
if len(queryModel.TagIds) > 0 {
query = query.Where("podcast_id in (select podcast_id from podcast_tags where tag_id in ?)", queryModel.TagIds)
}
if len(queryModel.PodcastIds) > 0 {
query = query.Where("podcast_id in ?", queryModel.PodcastIds)
}
totalsQuery := query.Order(getSortOrder(queryModel.Sorting)).Find(&podcasts)
totalsQuery.Count(&total)
result := query.Limit(queryModel.Count).Offset((queryModel.Page - 1) * queryModel.Count).Order("pub_date desc").Find(&podcasts)
return &podcasts, total, result.Error
}
func GetPaginatedPodcastItems(page int, count int, downloadedOnly *bool, playedOnly *bool, fromDate time.Time, podcasts *[]PodcastItem, total *int64) error {
query := DB.Preload("Podcast")
if downloadedOnly != nil {
@ -110,6 +170,22 @@ func GetAllPodcastItemsByPodcastIds(podcastIds []string, podcastItems *[]Podcast
result := DB.Preload(clause.Associations).Where("podcast_id in ?", podcastIds).Order("pub_date desc").Find(&podcastItems)
return result.Error
}
func GetAllPodcastItemsByIds(podcastItemIds []string) (*[]PodcastItem, error) {
var podcastItems []PodcastItem
var sb strings.Builder
sb.WriteString("\n CASE ID \n")
for i, v := range podcastItemIds {
sb.WriteString(fmt.Sprintf("WHEN '%v' THEN %v \n", v, i+1))
}
sb.WriteString(fmt.Sprintln("END"))
result := DB.Debug().Preload(clause.Associations).Where("id in ?", podcastItemIds).Order(sb.String()).Find(&podcastItems)
return &podcastItems, result.Error
}
func SetAllEpisodesToDownload(podcastId string) error {
result := DB.Model(PodcastItem{}).Where(&PodcastItem{PodcastID: podcastId, DownloadStatus: Deleted}).Update("download_status", NotDownloaded)

@ -0,0 +1,58 @@
package model
import "math"
type Pagination struct {
Page int `uri:"page" query:"page" json:"page" form:"page" default:1`
Count int `uri:"count" query:"count" json:"count" form:"count" default:20`
NextPage int `uri:"nextPage" query:"nextPage" json:"nextPage" form:"nextPage"`
PreviousPage int `uri:"previousPage" query:"previousPage" json:"previousPage" form:"previousPage"`
TotalCount int `uri:"totalCount" query:"totalCount" json:"totalCount" form:"totalCount"`
TotalPages int `uri:"totalPages" query:"totalPages" json:"totalPages" form:"totalPages"`
}
type EpisodeSort string
const (
RELEASE_ASC EpisodeSort = "release_asc"
RELEASE_DESC EpisodeSort = "release_desc"
DURATION_ASC EpisodeSort = "duration_asc"
DURATION_DESC EpisodeSort = "duration_desc"
)
type EpisodesFilter struct {
Pagination
IsDownloaded *string `uri:"isDownloaded" query:"isDownloaded" json:"isDownloaded" form:"isDownloaded"`
IsPlayed *string `uri:"isPlayed" query:"isPlayed" json:"isPlayed" form:"isPlayed"`
Sorting EpisodeSort `uri:"sorting" query:"sorting" json:"sorting" form:"sorting"`
Q string `uri:"q" query:"q" json:"q" form:"q"`
TagIds []string `uri:"tagIds" query:"tagIds[]" json:"tagIds" form:"tagIds[]"`
PodcastIds []string `uri:"podcastIds" query:"podcastIds[]" json:"podcastIds" form:"podcastIds[]"`
}
func (filter *EpisodesFilter) VerifyPaginationValues() {
if filter.Count == 0 {
filter.Count = 20
}
if filter.Page == 0 {
filter.Page = 1
}
if filter.Sorting == "" {
filter.Sorting = RELEASE_DESC
}
}
func (filter *EpisodesFilter) SetCounts(totalCount int64) {
totalPages := int(math.Ceil(float64(totalCount) / float64(filter.Count)))
nextPage, previousPage := 0, 0
if filter.Page < totalPages {
nextPage = filter.Page + 1
}
if filter.Page > 1 {
previousPage = filter.Page - 1
}
filter.NextPage = nextPage
filter.PreviousPage = previousPage
filter.TotalCount = int(totalCount)
filter.TotalPages = totalPages
}

@ -59,6 +59,10 @@ func GetPodcastItemById(id string) *db.PodcastItem {
return &podcastItem
}
func GetAllPodcastItemsByIds(podcastItemIds []string) (*[]db.PodcastItem, error) {
return db.GetAllPodcastItemsByIds(podcastItemIds)
}
func GetAllPodcastItemsByPodcastIds(podcastIds []string) *[]db.PodcastItem {
var podcastItems []db.PodcastItem

File diff suppressed because one or more lines are too long

@ -0,0 +1,402 @@
fieldset[disabled] .multiselect {
pointer-events: none;
}
.multiselect__spinner {
position: absolute;
right: 1px;
top: 1px;
width: 48px;
height: 35px;
background: #fff;
display: block;
}
.multiselect__spinner:after,
.multiselect__spinner:before {
position: absolute;
content: "";
top: 50%;
left: 50%;
margin: -8px 0 0 -8px;
width: 16px;
height: 16px;
border-radius: 100%;
border-color: #41b883 transparent transparent;
border-style: solid;
border-width: 2px;
box-shadow: 0 0 0 1px transparent;
}
.multiselect__spinner:before {
animation: a 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62);
animation-iteration-count: infinite;
}
.multiselect__spinner:after {
animation: a 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8);
animation-iteration-count: infinite;
}
.multiselect__loading-enter-active,
.multiselect__loading-leave-active {
transition: opacity 0.4s ease-in-out;
opacity: 1;
}
.multiselect__loading-enter,
.multiselect__loading-leave-active {
opacity: 0;
}
.multiselect,
.multiselect__input,
.multiselect__single {
font-family: inherit;
font-size: 16px;
-ms-touch-action: manipulation;
touch-action: manipulation;
}
.multiselect {
box-sizing: content-box;
display: block;
position: relative;
width: 100%;
min-height: 40px;
text-align: left;
color: #35495e;
}
.multiselect * {
box-sizing: border-box;
}
.multiselect:focus {
outline: none;
}
.multiselect--disabled {
opacity: 0.6;
}
.multiselect--active {
z-index: 1;
}
.multiselect--active:not(.multiselect--above) .multiselect__current,
.multiselect--active:not(.multiselect--above) .multiselect__input,
.multiselect--active:not(.multiselect--above) .multiselect__tags {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.multiselect--active .multiselect__select {
transform: rotate(180deg);
}
.multiselect--above.multiselect--active .multiselect__current,
.multiselect--above.multiselect--active .multiselect__input,
.multiselect--above.multiselect--active .multiselect__tags {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.multiselect__input,
.multiselect__single {
position: relative;
display: inline-block;
min-height: 20px;
line-height: 20px;
border: none;
border-radius: 5px;
padding: 0 0 0 5px;
width: 100%;
transition: border 0.1s ease;
box-sizing: border-box;
margin-bottom: 8px;
vertical-align: top;
}
.multiselect__input::-webkit-input-placeholder {
color: #35495e;
}
.multiselect__input:-ms-input-placeholder {
color: #35495e;
}
.multiselect__input::placeholder {
color: #35495e;
}
.multiselect__tag ~ .multiselect__input,
.multiselect__tag ~ .multiselect__single {
width: auto;
}
.multiselect__input:hover,
.multiselect__single:hover {
border-color: #cfcfcf;
}
.multiselect__input:focus,
.multiselect__single:focus {
border-color: #a8a8a8;
outline: none;
}
.multiselect__single {
padding-left: 5px;
margin-bottom: 8px;
}
.multiselect__tags-wrap {
display: inline;
}
.multiselect__tags {
min-height: 40px;
display: block;
padding: 8px 40px 0 8px;
border-radius: 5px;
border: 1px solid #e8e8e8;
font-size: 14px;
}
.multiselect__tag {
position: relative;
display: inline-block;
padding: 4px 26px 4px 10px;
border-radius: 5px;
margin-right: 10px;
color: #fff;
line-height: 1;
/* background: #41b883; */
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
}
.multiselect__tag-icon {
cursor: pointer;
margin-left: 7px;
position: absolute;
right: 0;
top: 0;
bottom: 0;
font-weight: 700;
font-style: normal;
width: 22px;
text-align: center;
line-height: 22px;
transition: all 0.2s ease;
border-radius: 5px;
}
.multiselect__tag-icon:after {
content: "\D7";
color: #266d4d;
font-size: 14px;
}
.multiselect__tag-icon:focus,
.multiselect__tag-icon:hover {
background: #369a6e;
}
.multiselect__tag-icon:focus:after,
.multiselect__tag-icon:hover:after {
color: #fff;
}
.multiselect__current {
min-height: 40px;
overflow: hidden;
padding: 8px 12px 0;
padding-right: 30px;
white-space: nowrap;
border-radius: 5px;
border: 1px solid #e8e8e8;
}
.multiselect__current,
.multiselect__select {
line-height: 16px;
box-sizing: border-box;
display: block;
margin: 0;
text-decoration: none;
cursor: pointer;
}
.multiselect__select {
position: absolute;
width: 40px;
height: 38px;
right: 1px;
top: 1px;
padding: 4px 8px;
text-align: center;
transition: transform 0.2s ease;
}
.multiselect__select:before {
position: relative;
right: 0;
top: 65%;
color: #999;
margin-top: 4px;
border-style: solid;
border-width: 5px 5px 0;
border-color: #999 transparent transparent;
content: "";
}
.multiselect__placeholder {
color: #adadad;
display: inline-block;
margin-bottom: 10px;
padding-top: 2px;
}
.multiselect--active .multiselect__placeholder {
display: none;
}
.multiselect__content-wrapper {
position: absolute;
display: block;
background: #fff;
width: 100%;
max-height: 240px;
overflow: auto;
border: 1px solid #e8e8e8;
border-top: none;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
z-index: 1;
-webkit-overflow-scrolling: touch;
}
.multiselect__content {
list-style: none;
display: inline-block;
padding: 0;
margin: 0;
min-width: 100%;
vertical-align: top;
}
.multiselect--above .multiselect__content-wrapper {
bottom: 100%;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom: none;
border-top: 1px solid #e8e8e8;
}
.multiselect__content::webkit-scrollbar {
display: none;
}
.multiselect__element {
display: block;
}
.multiselect__option {
display: block;
padding: 12px;
min-height: 40px;
line-height: 16px;
text-decoration: none;
text-transform: none;
vertical-align: middle;
position: relative;
cursor: pointer;
white-space: nowrap;
}
.multiselect__option:after {
top: 0;
right: 0;
position: absolute;
line-height: 40px;
padding-right: 12px;
padding-left: 20px;
font-size: 13px;
}
.multiselect__option--highlight {
/* background: #41b883; */
outline: none;
color: #fff;
}
.multiselect__option--highlight:after {
content: attr(data-select);
/* background: #41b883; */
color: #fff;
}
.multiselect__option--selected {
/* background: #f3f3f3;
color: #35495e; */
font-weight: 700;
}
.multiselect__option--selected:after {
content: attr(data-selected);
color: silver;
}
.multiselect__option--selected.multiselect__option--highlight {
/* background: #cccccc; */
/* color: #fff; */
}
.multiselect__option--selected.multiselect__option--highlight:after {
/* background: #cccccc; */
content: attr(data-deselect);
/* color: #fff; */
}
.multiselect--disabled {
background: #ededed;
pointer-events: none;
}
.multiselect--disabled .multiselect__current,
.multiselect--disabled .multiselect__select,
.multiselect__option--disabled {
background: #ededed;
color: #a6a6a6;
}
.multiselect__option--disabled {
cursor: text;
pointer-events: none;
}
.multiselect__option--group {
background: #ededed;
color: #35495e;
}
.multiselect__option--group.multiselect__option--highlight {
background: #35495e;
color: #fff;
}
.multiselect__option--group.multiselect__option--highlight:after {
background: #35495e;
}
.multiselect__option--disabled.multiselect__option--highlight {
background: #dedede;
}
.multiselect__option--group-selected.multiselect__option--highlight {
background: #ff6a6a;
color: #fff;
}
.multiselect__option--group-selected.multiselect__option--highlight:after {
background: #ff6a6a;
content: attr(data-deselect);
color: #fff;
}
.multiselect-enter-active,
.multiselect-leave-active {
transition: all 0.15s ease;
}
.multiselect-enter,
.multiselect-leave-active {
opacity: 0;
}
.multiselect__strong {
margin-bottom: 8px;
line-height: 20px;
display: inline-block;
vertical-align: top;
}
[dir="rtl"] .multiselect {
text-align: right;
}
[dir="rtl"] .multiselect__select {
right: auto;
left: 1px;
}
[dir="rtl"] .multiselect__tags {
padding: 8px 8px 0 40px;
}
[dir="rtl"] .multiselect__content {
text-align: right;
}
[dir="rtl"] .multiselect__option:after {
right: auto;
left: 0;
}
[dir="rtl"] .multiselect__clear {
right: auto;
left: 12px;
}
[dir="rtl"] .multiselect__spinner {
right: auto;
left: 1px;
}
@keyframes a {
0% {
transform: rotate(0);
}
to {
transform: rotate(2turn);
}
}

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