initial podcast support

This commit is contained in:
Thomas Amland 2021-01-24 19:06:39 +01:00
parent 353c57d819
commit 8022929dc1
8 changed files with 198 additions and 3 deletions

View File

@ -38,6 +38,10 @@
<Icon icon="star" /> Starred <Icon icon="star" /> Starred
</router-link> </router-link>
<router-link class="nav-link" :to="{name: 'podcasts'}">
<Icon icon="rss" /> Podcasts
</router-link>
<router-link class="nav-link" :to="{name: 'radio'}"> <router-link class="nav-link" :to="{name: 'radio'}">
<Icon icon="broadcast" /> Radio <Icon icon="broadcast" /> Radio
</router-link> </router-link>

View File

@ -54,9 +54,14 @@
</template> </template>
</td> </td>
<td v-if="!noAlbum" class="d-none d-md-table-cell"> <td v-if="!noAlbum" class="d-none d-md-table-cell">
<router-link :to="{name: 'album', params: {id: item.albumId}}" @click.native.stop> <template v-if="item.albumId">
<router-link :to="{name: 'album', params: {id: item.albumId}}" disabled @click.native.stop>
{{ item.album }}
</router-link>
</template>
<template v-else>
{{ item.album }} {{ item.album }}
</router-link> </template>
</td> </td>
<td v-if="!noDuration" class="text-right d-none d-md-table-cell"> <td v-if="!noDuration" class="text-right d-none d-md-table-cell">
{{ $formatDuration(item.duration) }} {{ $formatDuration(item.duration) }}

View File

@ -0,0 +1,88 @@
<template>
<ContentLoader v-slot :loading="podcast ==null">
<h1>{{ podcast.name }}</h1>
<p>{{ podcast.description }}</p>
<table class="table table-hover table-borderless table-numbered">
<thead>
<tr>
<th>
#
</th>
<th class="text-left">
Title
</th>
<th class="text-right d-none d-md-table-cell">
Duration
</th>
<th class="text-right">
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in podcast.tracks"
:key="index"
:class="{'active': item.id === playingTrackId, 'disabled': !item.playable}"
@click="click(item)">
<td>
<button>
<Icon class="icon" :icon="isPlaying && item.id === playingTrackId ? 'pause-fill' :'play-fill'" />
<span class="number">{{ item.track }}</span>
</button>
</td>
<td>
{{ item.title }}
<div class="text-muted">
<small>{{ item.description }}</small>
</div>
</td>
<td class="text-right d-none d-md-table-cell">
<template v-if="item.duration">
{{ $formatDuration(item.duration) }}
</template>
</td>
<td class="text-right" @click.stop="">
<OverflowMenu :disabled="!item.playable" />
</td>
</tr>
</tbody>
</table>
</ContentLoader>
</template>
<script lang="ts">
import Vue from 'vue'
import { mapGetters } from 'vuex'
export default Vue.extend({
props: {
id: { type: String, required: true },
},
data() {
return {
podcast: null as null | any,
}
},
computed: {
...mapGetters({
playingTrackId: 'player/trackId',
isPlaying: 'player/isPlaying',
}),
},
async created() {
this.podcast = await this.$api.getPodcast(this.id)
},
methods: {
async click(track: any) {
if (!track.playable) {
return
}
const tracks = this.podcast.tracks.filter((x: any) => x.playable)
const index = tracks.findIndex((x: any) => x.id === track.id)
return this.$store.dispatch('player/playTrackList', {
index,
tracks,
})
},
}
})
</script>

View File

@ -0,0 +1,42 @@
<template>
<ContentLoader v-slot :loading="items == null">
<div class="d-flex justify-content-between">
<h1>Podcasts</h1>
<OverflowMenu>
<b-dropdown-item-btn @click="refresh()">
Refresh
</b-dropdown-item-btn>
</OverflowMenu>
</div>
<Tiles square>
<Tile v-for="item in items" :key="item.id"
:image="item.image"
:to="{name: 'podcast', params: { id: item.id } }"
:title="item.name">
<template #text>
<strong>{{ item.trackCount }}</strong> episodes
</template>
</Tile>
</Tiles>
</ContentLoader>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
items: null as null | any[],
}
},
async created() {
this.items = await this.$api.getPodcasts()
},
methods: {
async refresh() {
await this.$api.refreshPodcasts()
this.items = await this.$api.getPodcasts()
}
}
})
</script>

View File

@ -25,7 +25,7 @@
{{ track.title }} {{ track.title }}
</div> </div>
<div class="text-truncate text-muted"> <div class="text-truncate text-muted">
{{ track.artist }} {{ track.artist || track.album || track.description }}
</div> </div>
</div> </div>
</template> </template>

View File

@ -318,6 +318,20 @@ export class API {
return this.get('rest/deleteInternetRadioStation', { id }) return this.get('rest/deleteInternetRadioStation', { id })
} }
async getPodcasts(): Promise<any[]> {
const response = await this.get('rest/getPodcasts')
return (response?.podcasts?.channel || []).map(this.normalizePodcast, this)
}
async getPodcast(id: string): Promise<any> {
const response = await this.get('rest/getPodcasts', { id })
return this.normalizePodcast(response?.podcasts?.channel[0])
}
async refreshPodcasts(): Promise<void> {
return this.get('rest/refreshPodcasts')
}
async scan(): Promise<void> { async scan(): Promise<void> {
return this.get('rest/startScan') return this.get('rest/startScan')
} }
@ -387,6 +401,33 @@ export class API {
} }
} }
private normalizePodcast(podcast: any): any {
const image = podcast.originalImageUrl
return {
id: podcast.id,
name: podcast.title,
description: podcast.description,
image: image,
url: podcast.url,
trackCount: podcast.episode.length,
tracks: podcast.episode.map((episode: any, index: number) => ({
id: episode.id,
title: episode.title,
duration: episode.duration,
starred: false,
track: podcast.episode.length - index,
album: podcast.title,
albumId: null,
artist: '',
artistId: null,
image,
url: episode.streamId ? this.getStreamUrl(episode.streamId) : null,
description: podcast.description,
playable: episode.status === 'completed',
})),
}
}
private getCoverArtUrl(item: any) { private getCoverArtUrl(item: any) {
if (!item.coverArt) { if (!item.coverArt) {
return undefined return undefined

View File

@ -26,6 +26,7 @@
BIconBoxArrowRight, BIconBoxArrowRight,
BIconPersonFill, BIconPersonFill,
BIconPersonCircle, BIconPersonCircle,
BIconRss,
BIconX, BIconX,
} from 'bootstrap-vue' } from 'bootstrap-vue'
@ -53,6 +54,7 @@
BIconBoxArrowRight, BIconBoxArrowRight,
BIconPersonFill, BIconPersonFill,
BIconPersonCircle, BIconPersonCircle,
BIconRss,
BIconX, BIconX,
}, },
props: { props: {

View File

@ -10,6 +10,8 @@ import GenreDetails from '@/library/genre/GenreDetails.vue'
import GenreLibrary from '@/library/genre/GenreLibrary.vue' import GenreLibrary from '@/library/genre/GenreLibrary.vue'
import Starred from '@/library/starred/Starred.vue' import Starred from '@/library/starred/Starred.vue'
import RadioStations from '@/library/radio/RadioStations.vue' import RadioStations from '@/library/radio/RadioStations.vue'
import PodcastDetails from '@/library/podcast/PodcastDetails.vue'
import PodcastLibrary from '@/library/podcast/PodcastLibrary.vue'
import Playlist from '@/playlist/Playlist.vue' import Playlist from '@/playlist/Playlist.vue'
import PlaylistList from '@/playlist/PlaylistList.vue' import PlaylistList from '@/playlist/PlaylistList.vue'
import SearchResult from '@/search/SearchResult.vue' import SearchResult from '@/search/SearchResult.vue'
@ -86,6 +88,17 @@ export function setupRouter(auth: AuthService) {
path: '/radio', path: '/radio',
component: RadioStations, component: RadioStations,
}, },
{
name: 'podcasts',
path: '/podcasts',
component: PodcastLibrary,
},
{
name: 'podcast',
path: '/podcast/:id',
component: PodcastDetails,
props: true,
},
{ {
name: 'playlists', name: 'playlists',
path: '/playlists', path: '/playlists',