[UI] Many improvements for the media UI.

- Support for _Play_ / _Play (With Cache)_ options for YouTube videos.

- Added `media.chromecast` and `media.gstreamer` UI panels.

- Removed `media.omxplayer` - the plugin has been removed.

- Enriched and improved the media info component.

- Propagate the media loading state to all children components.

- Persist query/search state on the URL.

Closes: #422
This commit is contained in:
Fabio Manganiello 2024-08-18 13:03:04 +02:00
parent a21aaee888
commit 01571e2e65
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
24 changed files with 307 additions and 105 deletions

View file

@ -53,15 +53,15 @@
"linode": { "linode": {
"class": "fas fa-cloud" "class": "fas fa-cloud"
}, },
"media.chromecast": {
"class": "fab fa-chromecast"
},
"media.jellyfin": { "media.jellyfin": {
"imgUrl": "/icons/jellyfin.svg" "imgUrl": "/icons/jellyfin.svg"
}, },
"media.kodi": { "media.kodi": {
"imgUrl": "/icons/kodi.svg" "imgUrl": "/icons/kodi.svg"
}, },
"media.omxplayer": {
"class": "fa fa-film"
},
"media.mplayer": { "media.mplayer": {
"class": "fa fa-film" "class": "fa fa-film"
}, },

View file

@ -57,7 +57,7 @@
</div> </div>
<div class="track-container col-s-9 col-m-9 col-l-3"> <div class="track-container col-s-9 col-m-9 col-l-3">
<div class="track-info" v-if="track && status?.state !== 'stop'"> <div class="track-info" @click="$emit('info', track)" v-if="track && status?.state !== 'stop'">
<div class="img-container" v-if="trackImage"> <div class="img-container" v-if="trackImage">
<img class="image from desktop" :src="trackImage" :alt="track.title"> <img class="image from desktop" :src="trackImage" :alt="track.title">
</div> </div>
@ -127,6 +127,7 @@ export default {
mixins: [Utils, MediaUtils], mixins: [Utils, MediaUtils],
emits: [ emits: [
'consume', 'consume',
'info',
'mute', 'mute',
'next', 'next',
'pause', 'pause',
@ -202,6 +203,9 @@ export default {
}, },
trackImage() { trackImage() {
if (this.track?.images?.length)
return this.track.images[0].url
return this.track?.image || this.image return this.track?.image || this.image
}, },
}, },
@ -405,6 +409,10 @@ button {
overflow: hidden; overflow: hidden;
align-items: center; align-items: center;
button {
background: none !important;
}
.row { .row {
width: 100%; width: 100%;
display: flex; display: flex;
@ -466,8 +474,14 @@ button {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.img-container {
max-width: 100%;
max-height: calc(100% + 3em);
}
.image { .image {
margin-right: 0.75em; padding: 0.5em;
max-height: 100%;
} }
} }
} }

View file

@ -9,6 +9,7 @@
:status="status" :status="status"
:track="track" :track="track"
@consume="$emit('consume', $event)" @consume="$emit('consume', $event)"
@info="$emit('info', $event)"
@mute="$emit('mute')" @mute="$emit('mute')"
@next="$emit('next')" @next="$emit('next')"
@pause="$emit('pause', $event)" @pause="$emit('pause', $event)"
@ -33,6 +34,7 @@ export default {
components: {Controls}, components: {Controls},
emits: [ emits: [
'consume', 'consume',
'info',
'mute', 'mute',
'next', 'next',
'pause', 'pause',

View file

@ -1,9 +1,7 @@
<template> <template>
<keep-alive> <keep-alive>
<div class="media-browser"> <div class="media-browser">
<Loading v-if="loading" /> <div class="media-index grid" v-if="!mediaProvider">
<div class="media-index grid" v-else-if="!mediaProvider">
<div class="item" <div class="item"
v-for="(provider, name) in mediaProviders" v-for="(provider, name) in mediaProviders"
:key="name" :key="name"
@ -19,10 +17,11 @@
</div> </div>
</div> </div>
<div class="media-browser-body" v-else-if="mediaProvider"> <div class="media-browser-body" v-if="mediaProvider">
<component <component
:is="mediaProvider" :is="mediaProvider"
:filter="filter" :filter="filter"
:loading="loading"
:selected-playlist="selectedPlaylist" :selected-playlist="selectedPlaylist"
:selected-channel="selectedChannel" :selected-channel="selectedChannel"
@add-to-playlist="$emit('add-to-playlist', $event)" @add-to-playlist="$emit('add-to-playlist', $event)"
@ -31,6 +30,7 @@
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@path-change="$emit('path-change', $event)" @path-change="$emit('path-change', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
/> />
</div> </div>
</div> </div>
@ -40,7 +40,6 @@
<script> <script>
import { defineAsyncComponent, ref } from "vue"; import { defineAsyncComponent, ref } from "vue";
import Browser from "@/components/File/Browser"; import Browser from "@/components/File/Browser";
import Loading from "@/components/Loading";
import Utils from "@/Utils"; import Utils from "@/Utils";
import providersMetadata from "./Providers/meta.json"; import providersMetadata from "./Providers/meta.json";
@ -54,6 +53,7 @@ export default {
'download-audio', 'download-audio',
'path-change', 'path-change',
'play', 'play',
'play-cache',
'remove-from-playlist', 'remove-from-playlist',
'remove-playlist', 'remove-playlist',
'rename-playlist', 'rename-playlist',
@ -61,7 +61,6 @@ export default {
components: { components: {
Browser, Browser,
Loading,
}, },
props: { props: {
@ -77,11 +76,15 @@ export default {
selectedChannel: { selectedChannel: {
type: Object, type: Object,
}, },
loading: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
loading: false,
mediaProvider: null, mediaProvider: null,
mediaProviders: {}, mediaProviders: {},
providersMetadata: providersMetadata, providersMetadata: providersMetadata,

View file

@ -67,10 +67,13 @@
</template> </template>
<script> <script>
import Players from "@/components/panels/Media/Players"; import Players from "@/components/panels/Media/Players"
import Utils from '@/Utils'
export default { export default {
name: "Header", name: "Header",
components: {Players}, components: {Players},
mixins: [Utils],
emits: [ emits: [
'filter', 'filter',
'filter-downloads', 'filter-downloads',
@ -132,20 +135,36 @@ export default {
} }
}, },
computed: {
enabledTypes() {
return Object.keys(this.sources).filter((source) => this.sources[source])
},
},
methods: { methods: {
search() { search() {
const types = Object.keys(this.sources).filter((source) => this.sources[source]) if (!this.query?.length || !this.enabledTypes?.length)
if (!this.query?.length || !types?.length)
return return
this.$emit('search', { this.$emit('search', {
query: this.query, query: this.query,
types: types, types: this.enabledTypes,
}) })
}, },
}, },
mounted() { mounted() {
this.$nextTick(() => {
const query = this.getUrlArgs()?.q
if (query) {
this.query = query
this.$emit('search', {
query: query,
types: this.enabledTypes,
})
}
})
this.$watch(() => this.selectedView, () => { this.$watch(() => this.selectedView, () => {
this.$emit('filter', '') this.$emit('filter', '')
this.torrentURL = '' this.torrentURL = ''

View file

@ -5,6 +5,7 @@
:status="selectedPlayer?.status || {}" :status="selectedPlayer?.status || {}"
:track="selectedPlayer?.status || {}" :track="selectedPlayer?.status || {}"
:buttons="mediaButtons" :buttons="mediaButtons"
@info="infoTrack = $event"
@play="pause" @play="pause"
@pause="pause" @pause="pause"
@stop="stop" @stop="stop"
@ -58,6 +59,7 @@
@open-channel="selectChannelFromItem" @open-channel="selectChannelFromItem"
@select="onResultSelect($event)" @select="onResultSelect($event)"
@play="play" @play="play"
@play-cache="play($event, {cache: true})"
@view="view" @view="view"
@download="download" @download="download"
@download-audio="downloadAudio" @download-audio="downloadAudio"
@ -78,6 +80,7 @@
/> />
<Browser :filter="browserFilter" <Browser :filter="browserFilter"
:loading="loading"
:selected-playlist="selectedPlaylist" :selected-playlist="selectedPlaylist"
:selected-channel="selectedChannel" :selected-channel="selectedChannel"
@add-to-playlist="addToPlaylistItem = $event" @add-to-playlist="addToPlaylistItem = $event"
@ -86,6 +89,7 @@
@download-audio="downloadAudio" @download-audio="downloadAudio"
@path-change="browserFilter = ''" @path-change="browserFilter = ''"
@play="play($event)" @play="play($event)"
@play-cache="play($event, {cache: true})"
v-else-if="selectedView === 'browser'" v-else-if="selectedView === 'browser'"
/> />
</div> </div>
@ -119,6 +123,20 @@
/> />
</Modal> </Modal>
</div> </div>
<div class="info-container" v-if="infoTrack != null">
<Modal ref="infoModal" title="Media info" :visible="infoTrack != null" @close="infoTrack = null">
<Info :item="infoTrack"
:pluginName="pluginName"
@add-to-playlist="addToPlaylistItem = $event"
@download="download"
@download-audio="downloadAudio"
@open-channel="selectChannelFromItem"
@play="play"
@play-cache="play($event, {cache: true})"
/>
</Modal>
</div>
</div> </div>
</keep-alive> </keep-alive>
</template> </template>
@ -129,6 +147,7 @@ import Utils from "@/Utils";
import Browser from "@/components/panels/Media/Browser"; import Browser from "@/components/panels/Media/Browser";
import Header from "@/components/panels/Media/Header"; import Header from "@/components/panels/Media/Header";
import Info from "@/components/panels/Media/Info";
import MediaDownloads from "@/components/panels/Media/Downloads"; import MediaDownloads from "@/components/panels/Media/Downloads";
import MediaUtils from "@/components/Media/Utils"; import MediaUtils from "@/components/Media/Utils";
import MediaView from "@/components/Media/View"; import MediaView from "@/components/Media/View";
@ -145,6 +164,7 @@ export default {
components: { components: {
Browser, Browser,
Header, Header,
Info,
MediaDownloads, MediaDownloads,
MediaView, MediaView,
Modal, Modal,
@ -176,33 +196,32 @@ export default {
data() { data() {
return { return {
loading: false,
results: [],
selectedResult: null,
selectedPlayer: null,
selectedView: 'search',
selectedSubtitles: null,
prevSelectedView: null,
showSubtitlesModal: false,
forceShowNav: false,
awaitingPlayTorrent: null,
urlPlay: null,
browserFilter: null,
downloadsFilter: null,
addToPlaylistItem: null, addToPlaylistItem: null,
torrentPlugin: null, awaitingPlayTorrent: null,
torrentPlugins: [ browserFilter: null,
'torrent', downloads: {},
'rtorrent', downloadsFilter: null,
], forceShowNav: false,
infoTrack: null,
loading: false,
prevSelectedView: null,
results: [],
selectedPlayer: null,
selectedResult: null,
selectedSubtitles: null,
selectedView: 'search',
showSubtitlesModal: false,
sources: { sources: {
'file': true, 'file': true,
'youtube': true, 'youtube': true,
'torrent': true, 'torrent': true,
}, },
urlPlay: null,
downloads: {}, torrentPlugin: null,
torrentPlugins: [
'torrent',
'rtorrent',
],
} }
}, },
@ -281,6 +300,7 @@ export default {
methods: { methods: {
async search(event) { async search(event) {
this.loading = true this.loading = true
this.setUrlArgs({q: event.query})
try { try {
this.results = await this.request(`${this.pluginName}.search`, event) this.results = await this.request(`${this.pluginName}.search`, event)
@ -289,7 +309,7 @@ export default {
} }
}, },
async play(item) { async play(item, opts) {
if (item?.type === 'torrent') { if (item?.type === 'torrent') {
this.awaitingPlayTorrent = item.url this.awaitingPlayTorrent = item.url
this.notify({ this.notify({
@ -309,7 +329,10 @@ export default {
if (!this.selectedPlayer.component.supports(item)) if (!this.selectedPlayer.component.supports(item))
item = await this.startStreaming(item, this.pluginName) item = await this.startStreaming(item, this.pluginName)
await this.selectedPlayer.component.play(item, this.selectedSubtitles, this.selectedPlayer) await this.selectedPlayer.component.play(
item, this.selectedSubtitles, this.selectedPlayer, opts
)
await this.refresh() await this.refresh()
} finally { } finally {
this.loading = false this.loading = false

View file

@ -1,28 +1,25 @@
<template> <template>
<div class="media-info"> <div class="media-info">
<div class="row header"> <div class="row header">
<div class="image-container"> <div class="item-container">
<MediaImage :item="item" @play="$emit('play')" @select="$emit('select')" /> <Item :item="item"
</div> @add-to-playlist="$emit('add-to-playlist', item)"
@open-channel="$emit('open-channel', item)"
<div class="title"> @play="$emit('play', item)"
<i :class="typeIcons[item.type]" @play-cache="$emit('play-cache', item)"
:title="item.type" @download="$emit('download', item)"
v-if="typeIcons[item?.type]"> @download-audio="$emit('download-audio', item)"
&nbsp; />
</i>
<a :href="item.url" target="_blank" v-if="item.url" v-text="item.title" />
<span v-else v-text="item.title" />
</div> </div>
</div> </div>
<div class="row direct-url" v-if="directUrl"> <div class="row direct-url" v-if="mainUrl">
<div class="left side">Direct URL</div> <div class="left side">Direct URL</div>
<div class="right side"> <div class="right side">
<a :href="directUrl" title="Direct URL" target="_blank"> <a :href="mainUrl" title="Direct URL" target="_blank">
<i class="fas fa-external-link-alt" /> <i class="fas fa-external-link-alt" />
</a> </a>
<button @click="copyToClipboard(directUrl)" title="Copy URL to clipboard"> <button @click="copyToClipboard(mainUrl)" title="Copy URL to clipboard">
<i class="fas fa-clipboard" /> <i class="fas fa-clipboard" />
</button> </button>
</div> </div>
@ -85,6 +82,11 @@
</div> </div>
</div> </div>
<div class="row" v-if="item?.view_count != null">
<div class="left side">Views</div>
<div class="right side">{{ formatNumber(item.view_count) }}</div>
</div>
<div class="row" v-if="item?.rating"> <div class="row" v-if="item?.rating">
<div class="left side">Rating</div> <div class="left side">Rating</div>
<div class="right side">{{ item.rating }}%</div> <div class="right side">{{ item.rating }}%</div>
@ -163,20 +165,34 @@
<div class="left side">Language</div> <div class="left side">Language</div>
<div class="right side" v-text="item.language" /> <div class="right side" v-text="item.language" />
</div> </div>
<div class="row" v-if="item?.audio_channels">
<div class="left side">Audio Channels</div>
<div class="right side" v-text="item.audio_channels" />
</div>
</div> </div>
</template> </template>
<script> <script>
import Utils from "@/Utils";
import MediaUtils from "@/components/Media/Utils";
import MediaImage from "./MediaImage";
import Icons from "./icons.json"; import Icons from "./icons.json";
import Item from "./Item";
import MediaUtils from "@/components/Media/Utils";
import Utils from "@/Utils";
export default { export default {
name: "Info", name: "Info",
components: {MediaImage}, components: {
Item,
},
mixins: [Utils, MediaUtils], mixins: [Utils, MediaUtils],
emits: ['play'], emits: [
'add-to-playlist',
'download',
'download-audio',
'open-channel',
'play',
'play-cache',
],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -224,6 +240,8 @@ export default {
return this.formatDate(this.item.publishedAt, true) return this.formatDate(this.item.publishedAt, true)
if (this.item?.created_at) if (this.item?.created_at)
return this.formatDate(this.item.created_at, true) return this.formatDate(this.item.created_at, true)
if (this.item?.timestamp)
return this.formatDate(this.item.timestamp, true)
return null return null
}, },
@ -236,6 +254,14 @@ export default {
return null return null
}, },
mainUrl() {
const directUrl = this.directUrl
if (directUrl)
return directUrl
return this.item?.url
},
}, },
} }
</script> </script>
@ -331,11 +357,9 @@ export default {
flex-direction: column; flex-direction: column;
position: relative; position: relative;
.image-container { .item-container {
@include from($desktop) { @include from($desktop) {
.image-container { width: 420px;
width: 420px;
}
} }
} }

View file

@ -15,6 +15,8 @@
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown"> <Dropdown title="Actions" icon-class="fa fa-ellipsis-h" ref="dropdown">
<DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')" <DropdownItem icon-class="fa fa-play" text="Play" @input="$emit('play')"
v-if="item.type !== 'torrent'" /> v-if="item.type !== 'torrent'" />
<DropdownItem icon-class="fa fa-play" text="Play (With Cache)" @input="$emit('play-cache')"
v-if="item.type === 'youtube'" />
<DropdownItem icon-class="fa fa-download" text="Download" @input="$emit('download')" <DropdownItem icon-class="fa fa-download" text="Download" @input="$emit('download')"
v-if="(item.type === 'torrent' || item.type === 'youtube') && item.item_type !== 'channel' && item.item_type !== 'playlist'" /> v-if="(item.type === 'torrent' || item.type === 'youtube') && item.item_type !== 'channel' && item.item_type !== 'playlist'" />
<DropdownItem icon-class="fa fa-volume-high" text="Download Audio" @input="$emit('download-audio')" <DropdownItem icon-class="fa fa-volume-high" text="Download Audio" @input="$emit('download-audio')"
@ -60,6 +62,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
'remove-from-playlist', 'remove-from-playlist',
'select', 'select',
'view', 'view',

View file

@ -13,7 +13,7 @@
</a> </a>
</span> </span>
<img class="image" :src="item.image" :alt="item.title" v-if="item?.image" /> <img class="image" :src="imgUrl" :alt="item.title" v-if="imgUrl" />
<div class="image" v-else> <div class="image" v-else>
<div class="inner"> <div class="inner">
<i :class="iconClass" /> <i :class="iconClass" />
@ -84,6 +84,15 @@ export default {
} }
}, },
imgUrl() {
let img = this.item?.image
if (!img) {
img = this.item?.images?.[0]?.url
}
return img
},
overlayIconClass() { overlayIconClass() {
if ( if (
this.item?.item_type === 'channel' || this.item?.item_type === 'channel' ||

View file

@ -9,8 +9,8 @@
@status="$emit('status', $event)" /> @status="$emit('status', $event)" />
<Mpv :player="selectedPlayer?.pluginName === 'media.mpv' ? selectedPlayer : null" ref="mpvPlugin" <Mpv :player="selectedPlayer?.pluginName === 'media.mpv' ? selectedPlayer : null" ref="mpvPlugin"
@status="$emit('status', $event)" /> @status="$emit('status', $event)" />
<Omxplayer :player="selectedPlayer?.pluginName === 'media.omxplayer' ? selectedPlayer : null" ref="omxplayerPlugin" <GStreamer :player="selectedPlayer?.pluginName === 'media.gstreamer' ? selectedPlayer : null"
@status="$emit('status', $event)" /> ref="gstreamerPlugin" @status="$emit('status', $event)" />
<Vlc :player="selectedPlayer?.pluginName === 'media.vlc' ? selectedPlayer : null" ref="vlcPlugin" <Vlc :player="selectedPlayer?.pluginName === 'media.vlc' ? selectedPlayer : null" ref="vlcPlugin"
@status="$emit('status', $event)" /> @status="$emit('status', $event)" />
</div> </div>
@ -40,18 +40,20 @@
import Dropdown from "@/components/elements/Dropdown"; import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem"; import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import Utils from '@/Utils'
import Chromecast from "@/components/panels/Media/Players/Chromecast" import Chromecast from "@/components/panels/Media/Players/Chromecast"
import Kodi from "@/components/panels/Media/Players/Kodi"; import Kodi from "@/components/panels/Media/Players/Kodi";
import Mplayer from "@/components/panels/Media/Players/Mplayer"; import Mplayer from "@/components/panels/Media/Players/Mplayer";
import Mpv from "@/components/panels/Media/Players/Mpv"; import Mpv from "@/components/panels/Media/Players/Mpv";
import Omxplayer from "@/components/panels/Media/Players/Omxplayer"; import GStreamer from "@/components/panels/Media/Players/GStreamer";
import Vlc from "@/components/panels/Media/Players/Vlc"; import Vlc from "@/components/panels/Media/Players/Vlc";
export default { export default {
name: "Players", name: "Players",
components: {Loading, DropdownItem, Dropdown, Chromecast, Kodi, Mplayer, Mpv, Omxplayer, Vlc}, components: {Loading, DropdownItem, Dropdown, Chromecast, Kodi, Mplayer, Mpv, GStreamer, Vlc},
emits: ['select', 'status'], emits: ['select', 'status'],
mixins: [Utils],
props: { props: {
pluginName: { pluginName: {
@ -89,7 +91,16 @@ export default {
this.players.push(...players) this.players.push(...players)
if (this.selectedPlayer == null && plugin.pluginName === this.pluginName && players.length > 0) { if (this.selectedPlayer == null && plugin.pluginName === this.pluginName && players.length > 0) {
this.select(players[0]) const urlSelectedPlayer = this.getUrlArgs().player
let player = players[0]
if (urlSelectedPlayer?.length) {
player = players.find((p) => p.name === urlSelectedPlayer)
if (!player)
player = players[0]
}
this.select(player)
} }
})) }))
} finally { } finally {

View file

@ -8,6 +8,7 @@ import Mixin from "@/components/panels/Media/Players/Mixin";
export default { export default {
name: "Chromecast", name: "Chromecast",
mixins: [Mixin], mixins: [Mixin],
emits: ['status'],
data() { data() {
return { return {
name: 'Chromecast', name: 'Chromecast',
@ -48,12 +49,20 @@ export default {
)?.status )?.status
}, },
async play(resource, player) { async play(resource, subs, player) {
if (!resource) { if (!resource) {
return await this.pause(player) return await this.pause(player)
} }
return await this.request(`${this.pluginName}.play`, {resource: resource.url, chromecast: this.getPlayerName(player)}) return await this.request(
`${this.pluginName}.play`,
{
resource: resource.url,
chromecast: this.getPlayerName(player),
subtitles: subs,
metadata: resource,
}
)
}, },
async pause(player) { async pause(player) {

View file

@ -6,13 +6,12 @@
import Mixin from "@/components/panels/Media/Players/Mixin"; import Mixin from "@/components/panels/Media/Players/Mixin";
export default { export default {
name: "Omxplayer",
mixins: [Mixin], mixins: [Mixin],
data() { data() {
return { return {
iconClass: 'fa fa-tv', iconClass: 'fa fa-tv',
name: 'OMXPlayer', name: 'GStreamer',
pluginName: 'media.omxplayer', pluginName: 'media.gstreamer',
} }
}, },
} }

View file

@ -36,12 +36,17 @@ export default {
return await this.request(`${this.pluginName}.status`) return await this.request(`${this.pluginName}.status`)
}, },
async play(resource, subs) { async play(resource, subs, _, opts) {
if (!resource) { if (!resource) {
return await this.pause() return await this.pause()
} }
return await this.request(`${this.pluginName}.play`, {resource: resource.url, subtitles: subs}) const args = {resource: resource.url, subtitles: subs, metadata: resource}
if (opts?.cache) {
args.cache_streams = true
}
return await this.request(`${this.pluginName}.play`, args)
}, },
async pause() { async pause() {

View file

@ -22,6 +22,11 @@ export default {
default: '', default: '',
}, },
loading: {
type: Boolean,
default: false,
},
selectedPlaylist: { selectedPlaylist: {
default: null, default: null,
}, },
@ -33,8 +38,14 @@ export default {
data() { data() {
return { return {
loading: false, loading_: false,
} }
}, },
computed: {
isLoading() {
return this.loading || this.loading_
},
},
} }
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="media-youtube-browser"> <div class="media-youtube-browser">
<Loading v-if="loading" /> <Loading v-if="loading_" />
<div class="browser" v-else> <div class="browser" v-else>
<MediaNav :path="computedPath" @back="$emit('back')" /> <MediaNav :path="computedPath" @back="$emit('back')" />
@ -8,32 +8,38 @@
<div class="body" v-else> <div class="body" v-else>
<Feed :filter="filter" <Feed :filter="filter"
:loading="isLoading"
@add-to-playlist="$emit('add-to-playlist', $event)" @add-to-playlist="$emit('add-to-playlist', $event)"
@download="$emit('download', $event)" @download="$emit('download', $event)"
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@open-channel="selectChannelFromItem" @open-channel="selectChannelFromItem"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
v-if="selectedView === 'feed'" v-if="selectedView === 'feed'"
/> />
<Playlists :filter="filter" <Playlists :filter="filter"
:loading="isLoading"
:selected-playlist="selectedPlaylist_" :selected-playlist="selectedPlaylist_"
@add-to-playlist="$emit('add-to-playlist', $event)" @add-to-playlist="$emit('add-to-playlist', $event)"
@download="$emit('download', $event)" @download="$emit('download', $event)"
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@open-channel="selectChannelFromItem" @open-channel="selectChannelFromItem"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
@remove-from-playlist="removeFromPlaylist" @remove-from-playlist="removeFromPlaylist"
@select="onPlaylistSelected" @select="onPlaylistSelected"
v-else-if="selectedView === 'playlists'" v-else-if="selectedView === 'playlists'"
/> />
<Subscriptions :filter="filter" <Subscriptions :filter="filter"
:loading="isLoading"
:selected-channel="selectedChannel_" :selected-channel="selectedChannel_"
@add-to-playlist="$emit('add-to-playlist', $event)" @add-to-playlist="$emit('add-to-playlist', $event)"
@download="$emit('download', $event)" @download="$emit('download', $event)"
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
@select="onChannelSelected" @select="onChannelSelected"
v-else-if="selectedView === 'subscriptions'" v-else-if="selectedView === 'subscriptions'"
/> />
@ -98,18 +104,18 @@ export default {
methods: { methods: {
async loadYoutubeConfig() { async loadYoutubeConfig() {
this.loading = true this.loading_ = true
try { try {
this.youtubeConfig = (await this.request('config.get_plugins')).youtube this.youtubeConfig = (await this.request('config.get_plugins')).youtube
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
async removeFromPlaylist(event) { async removeFromPlaylist(event) {
const playlistId = event.playlist_id const playlistId = event.playlist_id
const videoId = event.item.url const videoId = event.item.url
this.loading = true this.loading_ = true
try { try {
await this.request('youtube.remove_from_playlist', { await this.request('youtube.remove_from_playlist', {
@ -117,16 +123,16 @@ export default {
video_id: videoId, video_id: videoId,
}) })
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
async createPlaylist(name) { async createPlaylist(name) {
this.loading = true this.loading_ = true
try { try {
await this.request('youtube.create_playlist', {name: name}) await this.request('youtube.create_playlist', {name: name})
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="media-youtube-channel"> <div class="media-youtube-channel">
<Loading v-if="loading" /> <Loading v-if="isLoading" />
<div class="channel" v-else-if="channel"> <div class="channel" v-else-if="channel">
<div class="header"> <div class="header">
@ -29,7 +29,7 @@
</button> </button>
<div class="subscribers" v-if="channel.subscribers != null && (channel.subscribers || 0) >= 0"> <div class="subscribers" v-if="channel.subscribers != null && (channel.subscribers || 0) >= 0">
{{ channel.subscribers }} subscribers {{ formatNumber(channel.subscribers) }} subscribers
</div> </div>
</div> </div>
</div> </div>
@ -51,6 +51,7 @@
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@open-channel="$emit('open-channel', $event)" @open-channel="$emit('open-channel', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
@scroll-end="loadNextPage" @scroll-end="loadNextPage"
@select="selectedResult = $event" @select="selectedResult = $event"
/> />
@ -71,6 +72,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
], ],
components: { components: {
@ -88,12 +90,17 @@ export default {
type: String, type: String,
default: null, default: null,
}, },
loading: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
channel: null, channel: null,
loading: false, loading_: false,
loadingNextPage: false, loadingNextPage: false,
selectedResult: null, selectedResult: null,
subscribed: false, subscribed: false,
@ -101,6 +108,10 @@ export default {
}, },
computed: { computed: {
isLoading() {
return this.loading || this.loading_
},
itemsByUrl() { itemsByUrl() {
return this.channel?.items.reduce((acc, item) => { return this.channel?.items.reduce((acc, item) => {
acc[item.url] = item acc[item.url] = item
@ -111,12 +122,12 @@ export default {
methods: { methods: {
async loadChannel() { async loadChannel() {
this.loading = true this.loading_ = true
try { try {
await this.updateChannel(true) await this.updateChannel(true)
this.subscribed = await this.request('youtube.is_subscribed', {channel_id: this.id}) this.subscribed = await this.request('youtube.is_subscribed', {channel_id: this.id})
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="media-youtube-feed"> <div class="media-youtube-feed">
<Loading v-if="loading" /> <Loading v-if="isLoading" />
<NoItems :with-shadow="false" v-else-if="!feed?.length"> <NoItems :with-shadow="false" v-else-if="!feed?.length">
No videos found. No videos found.
</NoItems> </NoItems>
@ -15,6 +15,7 @@
@open-channel="$emit('open-channel', $event)" @open-channel="$emit('open-channel', $event)"
@select="selectedResult = $event" @select="selectedResult = $event"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
v-else /> v-else />
</div> </div>
</template> </template>
@ -33,6 +34,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
], ],
components: { components: {
@ -46,26 +48,37 @@ export default {
type: String, type: String,
default: null, default: null,
}, },
loading: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
feed: [], feed: [],
loading: false, loading_: false,
selectedResult: null, selectedResult: null,
} }
}, },
computed: {
isLoading() {
return this.loading_ || this.loading
},
},
methods: { methods: {
async loadFeed() { async loadFeed() {
this.loading = true this.loading_ = true
try { try {
this.feed = (await this.request('youtube.get_feed')).map(item => ({ this.feed = (await this.request('youtube.get_feed')).map(item => ({
...item, ...item,
type: 'youtube', type: 'youtube',
})) }))
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
}, },

View file

@ -54,6 +54,7 @@
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@open-channel="$emit('open-channel', $event)" @open-channel="$emit('open-channel', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
@remove-from-playlist="$emit('remove-from-playlist', $event)" @remove-from-playlist="$emit('remove-from-playlist', $event)"
@select="selectedResult = $event" @select="selectedResult = $event"
v-else /> v-else />
@ -75,6 +76,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
'remove-from-playlist', 'remove-from-playlist',
], ],

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="media-youtube-playlists"> <div class="media-youtube-playlists">
<div class="playlists-index" v-if="!selectedPlaylist?.id"> <div class="playlists-index" v-if="!selectedPlaylist?.id">
<Loading v-if="loading" /> <Loading v-if="isLoading" />
<NoItems :with-shadow="false" v-else-if="!playlists?.length"> <NoItems :with-shadow="false" v-else-if="!playlists?.length">
No playlists found. No playlists found.
</NoItems> </NoItems>
@ -36,6 +36,7 @@
@open-channel="$emit('open-channel', $event)" @open-channel="$emit('open-channel', $event)"
@remove-from-playlist="$emit('remove-from-playlist', {item: $event, playlist_id: selectedPlaylist.id})" @remove-from-playlist="$emit('remove-from-playlist', {item: $event, playlist_id: selectedPlaylist.id})"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
/> />
</div> </div>
@ -117,6 +118,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
'remove-from-playlist', 'remove-from-playlist',
'remove-playlist', 'remove-playlist',
'rename-playlist', 'rename-playlist',
@ -144,6 +146,11 @@ export default {
type: String, type: String,
default: null, default: null,
}, },
loading: {
type: Boolean,
default: false,
},
}, },
data() { data() {
@ -153,7 +160,7 @@ export default {
editedPlaylistName: '', editedPlaylistName: '',
editedPlaylistDescription: '', editedPlaylistDescription: '',
playlists: [], playlists: [],
loading: false, loading_: false,
showCreatePlaylist: false, showCreatePlaylist: false,
} }
}, },
@ -167,26 +174,30 @@ export default {
return acc return acc
}, {}) }, {})
}, },
isLoading() {
return this.loading_ || this.loading
},
}, },
methods: { methods: {
async loadPlaylists() { async loadPlaylists() {
this.loading = true this.loading_ = true
try { try {
this.playlists = (await this.request('youtube.get_playlists')) this.playlists = (await this.request('youtube.get_playlists'))
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
async createPlaylist(name) { async createPlaylist(name) {
this.loading = true this.loading_ = true
try { try {
await this.request('youtube.create_playlist', {name: name}) await this.request('youtube.create_playlist', {name: name})
this.showCreatePlaylist = false this.showCreatePlaylist = false
this.loadPlaylists() this.loadPlaylists()
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
@ -194,13 +205,13 @@ export default {
if (!this.deletedPlaylist) if (!this.deletedPlaylist)
return return
this.loading = true this.loading_ = true
try { try {
await this.request('youtube.delete_playlist', {id: this.deletedPlaylist}) await this.request('youtube.delete_playlist', {id: this.deletedPlaylist})
this.deletedPlaylist = null this.deletedPlaylist = null
this.loadPlaylists() this.loadPlaylists()
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },
@ -208,7 +219,7 @@ export default {
if (!this.editedPlaylist) if (!this.editedPlaylist)
return return
this.loading = true this.loading_ = true
try { try {
await this.request('youtube.rename_playlist', { await this.request('youtube.rename_playlist', {
id: this.editedPlaylist, id: this.editedPlaylist,
@ -219,7 +230,7 @@ export default {
this.clearEditPlaylist() this.clearEditPlaylist()
this.loadPlaylists() this.loadPlaylists()
} finally { } finally {
this.loading = false this.loading_ = false
} }
}, },

View file

@ -27,6 +27,7 @@
@download="$emit('download', $event)" @download="$emit('download', $event)"
@download-audio="$emit('download-audio', $event)" @download-audio="$emit('download-audio', $event)"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@play-cache="$emit('play-cache', $event)"
/> />
</div> </div>
</div> </div>
@ -45,6 +46,7 @@ export default {
'download', 'download',
'download-audio', 'download-audio',
'play', 'play',
'play-cache',
'select', 'select',
], ],

View file

@ -13,6 +13,7 @@
@remove-from-playlist="$emit('remove-from-playlist', item)" @remove-from-playlist="$emit('remove-from-playlist', item)"
@select="$emit('select', i)" @select="$emit('select', i)"
@play="$emit('play', item)" @play="$emit('play', item)"
@play-cache="$emit('play-cache', item)"
@view="$emit('view', item)" @view="$emit('view', item)"
@download="$emit('download', item)" @download="$emit('download', item)"
@download-audio="$emit('download-audio', item)" @download-audio="$emit('download-audio', item)"
@ -22,7 +23,12 @@
<Modal ref="infoModal" title="Media info" @close="$emit('select', null)"> <Modal ref="infoModal" title="Media info" @close="$emit('select', null)">
<Info :item="results[selectedResult]" <Info :item="results[selectedResult]"
:pluginName="pluginName" :pluginName="pluginName"
@add-to-playlist="$emit('add-to-playlist', results[selectedResult])"
@download="$emit('download', results[selectedResult])"
@download-audio="$emit('download-audio', results[selectedResult])"
@open-channel="$emit('open-channel', results[selectedResult])"
@play="$emit('play', results[selectedResult])" @play="$emit('play', results[selectedResult])"
@play-cache="$emit('play-cache', results[selectedResult])"
v-if="selectedResult != null" /> v-if="selectedResult != null" />
</Modal> </Modal>
</div> </div>
@ -42,6 +48,7 @@ export default {
'download-audio', 'download-audio',
'open-channel', 'open-channel',
'play', 'play',
'play-cache',
'remove-from-playlist', 'remove-from-playlist',
'scroll-end', 'scroll-end',
'select', 'select',

View file

@ -1,12 +1,11 @@
<template> <template>
<Media plugin-name="media.omxplayer" /> <Media plugin-name="media.chromecast" />
</template> </template>
<script> <script>
import Media from '@/components/panels/Media/Index' import Media from '@/components/panels/Media/Index'
export default { export default {
name: "MediaMpv",
components: {Media}, components: {Media},
} }
</script> </script>

View file

@ -0,0 +1,15 @@
<template>
<Media plugin-name="media.gstreamer" />
</template>
<script>
import Media from '@/components/panels/Media/Index'
export default {
components: {Media},
}
</script>
<style scoped>
</style>

View file

@ -16,6 +16,10 @@ export default {
indent(text, spaces = 2) { indent(text, spaces = 2) {
return text.split('\n').map((t) => `${' '.repeat(spaces)}${t}`).join('\n') return text.split('\n').map((t) => `${' '.repeat(spaces)}${t}`).join('\n')
}, },
formatNumber(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
},
}, },
} }
</script> </script>