forked from platypush/platypush
Added frontend support for Plex
This commit is contained in:
parent
85f56cf98c
commit
370a7d4c15
77 changed files with 575 additions and 220 deletions
1
platypush/backend/http/dist/icons/plex.svg
vendored
Normal file
1
platypush/backend/http/dist/icons/plex.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="15%" fill="#282a2d"/><path d="M256 70H148l108 186-108 186h108l108-186z" fill="#e5a00d"/></svg>
|
After Width: | Height: | Size: 191 B |
2
platypush/backend/http/dist/index.html
vendored
2
platypush/backend/http/dist/index.html
vendored
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-092add3f.ef0e3fb1.css" rel="prefetch"><link href="/static/css/chunk-17cd846b.ef0e3fb1.css" rel="prefetch"><link href="/static/css/chunk-18cd0234.ef0e3fb1.css" rel="prefetch"><link href="/static/css/chunk-24ff873d.934c66a7.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.120a35b6.css" rel="prefetch"><link href="/static/css/chunk-3d60f62e.804fa9fd.css" rel="prefetch"><link href="/static/css/chunk-4201fea8.d5bec80e.css" rel="prefetch"><link href="/static/css/chunk-427f1aa2.3bfca7ea.css" rel="prefetch"><link href="/static/css/chunk-432b400a.348c1f35.css" rel="prefetch"><link href="/static/css/chunk-45939517.f01c29cc.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.1a24453a.css" rel="prefetch"><link href="/static/css/chunk-4bc2706b.85a389d1.css" rel="prefetch"><link href="/static/css/chunk-545459d0.6b856cb1.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.a5b70794.css" rel="prefetch"><link href="/static/css/chunk-9684cd10.cb260423.css" rel="prefetch"><link href="/static/css/chunk-a3d822ca.ef0e3fb1.css" rel="prefetch"><link href="/static/css/chunk-a6931176.29aa43aa.css" rel="prefetch"><link href="/static/css/chunk-cd9a889e.63f19efc.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.105050d2.css" rel="prefetch"><link href="/static/css/chunk-e8078048.9f279ae9.css" rel="prefetch"><link href="/static/js/chunk-092add3f.f11cf693.js" rel="prefetch"><link href="/static/js/chunk-17cd846b.10943176.js" rel="prefetch"><link href="/static/js/chunk-18cd0234.21a689a3.js" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d0cc2be.b3a583d9.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-2d237d41.3427f74b.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.9a57c504.js" rel="prefetch"><link href="/static/js/chunk-3d60f62e.907e4050.js" rel="prefetch"><link href="/static/js/chunk-4201fea8.29361f0f.js" rel="prefetch"><link href="/static/js/chunk-427f1aa2.0a437283.js" rel="prefetch"><link href="/static/js/chunk-432b400a.719d7f81.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-4bc2706b.38882fe9.js" rel="prefetch"><link href="/static/js/chunk-545459d0.da1ea7e5.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-9684cd10.7051bb65.js" rel="prefetch"><link href="/static/js/chunk-a3d822ca.ac168508.js" rel="prefetch"><link href="/static/js/chunk-a6931176.7f85fbf3.js" rel="prefetch"><link href="/static/js/chunk-cd9a889e.ec43fdb3.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/css/app.0bb8bfb7.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.d6d33181.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.ac361ae9.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.0bb8bfb7.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.ac361ae9.js"></script><script src="/static/js/app.d6d33181.js"></script></body></html>
|
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-076c5199.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-1293e286.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-14f3b6ed.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-24ff873d.25b446f2.css" rel="prefetch"><link href="/static/css/chunk-35b45d59.3285a5c5.css" rel="prefetch"><link href="/static/css/chunk-3d60f62e.4fa298b8.css" rel="prefetch"><link href="/static/css/chunk-4201fea8.ab45ee69.css" rel="prefetch"><link href="/static/css/chunk-45939517.ce9c914b.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.8b96bf08.css" rel="prefetch"><link href="/static/css/chunk-4bc2706b.1e09b17a.css" rel="prefetch"><link href="/static/css/chunk-545459d0.4806f03d.css" rel="prefetch"><link href="/static/css/chunk-59396623.5084b7e8.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.d329d923.css" rel="prefetch"><link href="/static/css/chunk-9684cd10.6046f7ac.css" rel="prefetch"><link href="/static/css/chunk-abbc1cdc.fc1132f4.css" rel="prefetch"><link href="/static/css/chunk-cb418146.1cc7af9d.css" rel="prefetch"><link href="/static/css/chunk-cd9a889e.b5ec8fac.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.0d73779a.css" rel="prefetch"><link href="/static/css/chunk-e8078048.04620e86.css" rel="prefetch"><link href="/static/css/chunk-fa20b8a0.d7316f99.css" rel="prefetch"><link href="/static/js/chunk-076c5199.377e9834.js" rel="prefetch"><link href="/static/js/chunk-1293e286.eb2fa695.js" rel="prefetch"><link href="/static/js/chunk-14f3b6ed.d6fcafdc.js" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d0cc2be.b3a583d9.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-2d237d41.3427f74b.js" rel="prefetch"><link href="/static/js/chunk-35b45d59.9a57c504.js" rel="prefetch"><link href="/static/js/chunk-3d60f62e.907e4050.js" rel="prefetch"><link href="/static/js/chunk-4201fea8.29361f0f.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-4bc2706b.38882fe9.js" rel="prefetch"><link href="/static/js/chunk-545459d0.da1ea7e5.js" rel="prefetch"><link href="/static/js/chunk-59396623.19b5fca7.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-9684cd10.7051bb65.js" rel="prefetch"><link href="/static/js/chunk-abbc1cdc.47491a05.js" rel="prefetch"><link href="/static/js/chunk-cb418146.7a824439.js" rel="prefetch"><link href="/static/js/chunk-cd9a889e.ec43fdb3.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/js/chunk-fa20b8a0.78555b70.js" rel="prefetch"><link href="/static/css/app.9ee642c5.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.1abebcd8.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.30e3a6cb.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.9ee642c5.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.30e3a6cb.js"></script><script src="/static/js/app.1abebcd8.js"></script></body></html>
|
File diff suppressed because one or more lines are too long
15
platypush/backend/http/dist/static/css/chunk-076c5199.fc1132f4.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-076c5199.fc1132f4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
15
platypush/backend/http/dist/static/css/chunk-1293e286.fc1132f4.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-1293e286.fc1132f4.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
platypush/backend/http/dist/static/css/chunk-14f3b6ed.fc1132f4.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-14f3b6ed.fc1132f4.css
vendored
Normal file
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
File diff suppressed because one or more lines are too long
15
platypush/backend/http/dist/static/css/chunk-abbc1cdc.fc1132f4.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-abbc1cdc.fc1132f4.css
vendored
Normal file
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 one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
15
platypush/backend/http/dist/static/css/chunk-fa20b8a0.d7316f99.css
vendored
Normal file
15
platypush/backend/http/dist/static/css/chunk-fa20b8a0.d7316f99.css
vendored
Normal file
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 one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-076c5199.377e9834.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-076c5199.377e9834.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-076c5199.377e9834.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-076c5199.377e9834.js.map
vendored
Normal file
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 one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-1293e286.eb2fa695.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-1293e286.eb2fa695.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-1293e286.eb2fa695.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-1293e286.eb2fa695.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-14f3b6ed.d6fcafdc.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-14f3b6ed.d6fcafdc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-14f3b6ed.d6fcafdc.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-14f3b6ed.d6fcafdc.js.map
vendored
Normal file
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-59396623.19b5fca7.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-59396623.19b5fca7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-59396623.19b5fca7.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-59396623.19b5fca7.js.map
vendored
Normal file
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 one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-abbc1cdc.47491a05.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-abbc1cdc.47491a05.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-abbc1cdc.47491a05.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-abbc1cdc.47491a05.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-cb418146.7a824439.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-cb418146.7a824439.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-cb418146.7a824439.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-cb418146.7a824439.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
platypush/backend/http/dist/static/js/chunk-fa20b8a0.78555b70.js
vendored
Normal file
2
platypush/backend/http/dist/static/js/chunk-fa20b8a0.78555b70.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/dist/static/js/chunk-fa20b8a0.78555b70.js.map
vendored
Normal file
1
platypush/backend/http/dist/static/js/chunk-fa20b8a0.78555b70.js.map
vendored
Normal file
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 one or more lines are too long
1
platypush/backend/http/webapp/public/icons/plex.svg
Normal file
1
platypush/backend/http/webapp/public/icons/plex.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="15%" fill="#282a2d"/><path d="M256 70H148l108 186-108 186h108l108-186z" fill="#e5a00d"/></svg>
|
After Width: | Height: | Size: 191 B |
107
platypush/backend/http/webapp/src/components/File/Browser.vue
Normal file
107
platypush/backend/http/webapp/src/components/File/Browser.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<div class="browser-container">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
|
<div class="row item" @click="path = (path || '') + '/..'" v-if="path?.length && path !== '/'">
|
||||||
|
<div class="col-10 left side">
|
||||||
|
<i class="icon fa fa-folder" />
|
||||||
|
<span class="name">..</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row item" v-for="(file, i) in filteredFiles" :key="i" @click="path = file.path">
|
||||||
|
<div class="col-10">
|
||||||
|
<i class="icon fa" :class="{'fa-file': file.type !== 'directory', 'fa-folder': file.type === 'directory'}" />
|
||||||
|
<span class="name">
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-2 actions">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownItem icon-class="fa fa-play" text="Play"
|
||||||
|
@click="$emit('play', {type: 'file', url: `file://${file.path}`})"
|
||||||
|
v-if="isMedia && mediaExtensions.has(file.name.split('.').pop())" />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
|
import Dropdown from "@/components/elements/Dropdown";
|
||||||
|
import DropdownItem from "@/components/elements/DropdownItem";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Browser",
|
||||||
|
components: {DropdownItem, Dropdown, Loading},
|
||||||
|
mixins: [Utils, MediaUtils],
|
||||||
|
emits: ['path-change'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initialPath: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
isMedia: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
filter: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
path: this.initialPath,
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredFiles() {
|
||||||
|
if (!this.filter?.length)
|
||||||
|
return this.files
|
||||||
|
|
||||||
|
return this.files.filter((file) => (file?.name || '').toLowerCase().indexOf(this.filter.toLowerCase()) >= 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.files = await this.request('file.list', {path: this.path})
|
||||||
|
this.$emit('path-change', this.path)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$watch(() => this.path, () => this.refresh())
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "src/style/items";
|
||||||
|
|
||||||
|
.browser-container {
|
||||||
|
.item {
|
||||||
|
.actions {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -53,8 +53,7 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.view-container {
|
.view-container {
|
||||||
height: calc(100% - #{$media-ctrl-panel-height});
|
height: 100%;
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-container {
|
.controls-container {
|
||||||
|
|
|
@ -52,9 +52,10 @@ export default {
|
||||||
|
|
||||||
let element = event.target
|
let element = event.target
|
||||||
while (element) {
|
while (element) {
|
||||||
if (element === this.$refs.dropdown.element) {
|
if (!this.$refs.dropdown)
|
||||||
|
break
|
||||||
|
if (element === this.$refs.dropdown.element)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentElement
|
element = element.parentElement
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
<div class="header" :class="{'with-filter': filterVisible}">
|
<div class="header" :class="{'with-filter': filterVisible}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 left side" v-if="selectedView === 'search'">
|
<div class="col-7 left side" v-if="selectedView === 'search'">
|
||||||
<button title="Filter" @click="filterVisible = !filterVisible">
|
<button title="Filter" class="filter-btn" :class="{selected: filterVisible}"
|
||||||
|
@click="filterVisible = !filterVisible">
|
||||||
<i class="fa fa-filter" />
|
<i class="fa fa-filter" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -21,6 +22,13 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-7 left side" v-else-if="selectedView === 'browser'">
|
||||||
|
<label class="search-box">
|
||||||
|
<input type="search" placeholder="Filter" :value="browserFilter" @change="$emit('filter', $event.target.value)"
|
||||||
|
@keyup="$emit('filter', $event.target.value)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-5 right side">
|
<div class="col-5 right side">
|
||||||
<button title="Select subtitles" class="captions-btn" :class="{selected: selectedSubtitles != null}"
|
<button title="Select subtitles" class="captions-btn" :class="{selected: selectedSubtitles != null}"
|
||||||
@click="$emit('show-subtitles')" v-if="hasSubtitlesPlugin && selectedItem &&
|
@click="$emit('show-subtitles')" v-if="hasSubtitlesPlugin && selectedItem &&
|
||||||
|
@ -30,15 +38,16 @@
|
||||||
|
|
||||||
<Players :plugin-name="pluginName" @select="$emit('select-player', $event)"
|
<Players :plugin-name="pluginName" @select="$emit('select-player', $event)"
|
||||||
@status="$emit('player-status', $event)" />
|
@status="$emit('player-status', $event)" />
|
||||||
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
|
|
||||||
<DropdownItem text="Play URL" icon-class="fa fa-play-circle" />
|
<button title="Play URL" @click="$emit('play-url')">
|
||||||
</Dropdown>
|
<i class="fa fa-plus-circle" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row filter fade-in" :class="{hidden: !filterVisible}">
|
<div class="row filter fade-in" :class="{hidden: !filterVisible}">
|
||||||
<label v-for="source in Object.keys(sources)" :key="source">
|
<label v-for="source in Object.keys(sources)" :key="source">
|
||||||
<input type="checkbox" v-model="sources[source]" />
|
<input type="checkbox" :checked="sources[source]" @change="$emit('source-toggle', source)" />
|
||||||
{{ source }}
|
{{ source }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,13 +55,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Dropdown from "@/components/elements/Dropdown";
|
|
||||||
import DropdownItem from "@/components/elements/DropdownItem";
|
|
||||||
import Players from "@/components/panels/Media/Players";
|
import Players from "@/components/panels/Media/Players";
|
||||||
export default {
|
export default {
|
||||||
name: "Header",
|
name: "Header",
|
||||||
components: {Players, DropdownItem, Dropdown},
|
components: {Players},
|
||||||
emits: ['search', 'select-player', 'player-status', 'torrent-add', 'show-subtitles'],
|
emits: ['search', 'select-player', 'player-status', 'torrent-add', 'show-subtitles', 'play-url', 'filter',
|
||||||
|
'source-toggle'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
pluginName: {
|
pluginName: {
|
||||||
|
@ -77,6 +85,16 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
browserFilter: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -84,11 +102,6 @@ export default {
|
||||||
filterVisible: false,
|
filterVisible: false,
|
||||||
query: '',
|
query: '',
|
||||||
torrentURL: '',
|
torrentURL: '',
|
||||||
sources: {
|
|
||||||
'file': true,
|
|
||||||
'youtube': true,
|
|
||||||
'torrent': true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -103,7 +116,15 @@ export default {
|
||||||
types: types,
|
types: types,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$watch(() => this.selectedView, () => {
|
||||||
|
this.$emit('filter', '')
|
||||||
|
this.torrentURL = ''
|
||||||
|
this.query = ''
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -118,6 +139,10 @@ export default {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
box-shadow: $border-shadow-bottom;
|
box-shadow: $border-shadow-bottom;
|
||||||
|
|
||||||
|
.filter-btn.selected {
|
||||||
|
color: $selected-fg;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -125,6 +150,7 @@ export default {
|
||||||
|
|
||||||
&.with-filter {
|
&.with-filter {
|
||||||
height: calc(#{$media-header-height} + #{$filter-header-height});
|
height: calc(#{$media-header-height} + #{$filter-header-height});
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side {
|
.side {
|
||||||
|
@ -166,10 +192,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
top: $media-header-height;
|
|
||||||
height: $filter-header-height;
|
height: $filter-header-height;
|
||||||
padding-bottom: 1em;
|
margin-top: .5em;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
@ -13,19 +13,24 @@
|
||||||
|
|
||||||
<div class="view-container">
|
<div class="view-container">
|
||||||
<Header :plugin-name="pluginName" :selected-view="selectedView" :has-subtitles-plugin="hasSubtitlesPlugin"
|
<Header :plugin-name="pluginName" :selected-view="selectedView" :has-subtitles-plugin="hasSubtitlesPlugin"
|
||||||
:selected-item="selectedPlayer && selectedPlayer.status &&
|
ref="header" :sources="sources" :selected-item="selectedPlayer && selectedPlayer.status &&
|
||||||
(selectedPlayer.status.state === 'play' || selectedPlayer.status.state === 'pause')
|
(selectedPlayer.status.state === 'play' || selectedPlayer.status.state === 'pause')
|
||||||
? selectedPlayer.status : results[selectedResult]" :selected-subtitles="selectedSubtitles"
|
? selectedPlayer.status : results[selectedResult]" :selected-subtitles="selectedSubtitles"
|
||||||
@search="search" @select-player="selectedPlayer = $event" @player-status="onStatusUpdate"
|
:browser-filter="browserFilter" @search="search" @select-player="selectedPlayer = $event"
|
||||||
@torrent-add="downloadTorrent($event)" @show-subtitles="showSubtitlesModal = !showSubtitlesModal" />
|
@player-status="onStatusUpdate" @torrent-add="downloadTorrent($event)"
|
||||||
|
@show-subtitles="showSubtitlesModal = !showSubtitlesModal" @play-url="$refs.playUrlModal.show()"
|
||||||
|
@filter="browserFilter = $event" @source-toggle="sources[$event] = !sources[$event]" />
|
||||||
|
|
||||||
<div class="body-container">
|
<div class="body-container" :class="{'expanded-header': $refs.header?.filterVisible}">
|
||||||
<Results :results="results" :selected-result="selectedResult" @select="onResultSelect($event)"
|
<Results :results="results" :selected-result="selectedResult" @select="onResultSelect($event)"
|
||||||
@play="play" @info="$refs.mediaInfo.isVisible = true" @view="view" @download="download"
|
@play="play" @info="$refs.mediaInfo.isVisible = true" @view="view" @download="download"
|
||||||
v-if="selectedView === 'search'" />
|
:sources="sources" v-if="selectedView === 'search'" />
|
||||||
|
|
||||||
<TorrentView :plugin-name="torrentPlugin" :is-media="true" @play="play"
|
<TorrentView :plugin-name="torrentPlugin" :is-media="true" @play="play"
|
||||||
v-else-if="selectedView === 'torrents'" />
|
v-else-if="selectedView === 'torrents'" />
|
||||||
|
|
||||||
|
<Browser :plugin-name="torrentPlugin" :is-media="true" :filter="browserFilter"
|
||||||
|
@path-change="browserFilter = ''" @play="play($event)" v-else-if="selectedView === 'browser'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -47,6 +52,23 @@
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="play-url-container">
|
||||||
|
<Modal title="Play URL" ref="playUrlModal" @open="$refs.playUrlInput.focus()">
|
||||||
|
<form @submit.prevent="playUrl(urlPlay)">
|
||||||
|
<div class="row">
|
||||||
|
<label>
|
||||||
|
Play URL (use <tt>file://</tt> prefix for local files)
|
||||||
|
<input type="text" v-model="urlPlay" ref="playUrlInput" autofocus />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row footer">
|
||||||
|
<button type="submit" :disabled="!urlPlay?.length">Play</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</template>
|
</template>
|
||||||
|
@ -63,11 +85,12 @@ import Nav from "@/components/panels/Media/Nav";
|
||||||
import Results from "@/components/panels/Media/Results";
|
import Results from "@/components/panels/Media/Results";
|
||||||
import Subtitles from "@/components/panels/Media/Subtitles";
|
import Subtitles from "@/components/panels/Media/Subtitles";
|
||||||
import TorrentView from "@/components/panels/Torrent/View";
|
import TorrentView from "@/components/panels/Torrent/View";
|
||||||
|
import Browser from "@/components/File/Browser";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Media",
|
name: "Media",
|
||||||
mixins: [Utils, MediaUtils],
|
mixins: [Utils, MediaUtils],
|
||||||
components: {Loading, MediaView, Header, Results, Modal, Info, Nav, TorrentView, Subtitles},
|
components: {Browser, Loading, MediaView, Header, Results, Modal, Info, Nav, TorrentView, Subtitles},
|
||||||
props: {
|
props: {
|
||||||
pluginName: {
|
pluginName: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -96,11 +119,19 @@ export default {
|
||||||
selectedSubtitles: null,
|
selectedSubtitles: null,
|
||||||
showSubtitlesModal: false,
|
showSubtitlesModal: false,
|
||||||
awaitingPlayTorrent: null,
|
awaitingPlayTorrent: null,
|
||||||
|
urlPlay: null,
|
||||||
|
browserFilter: null,
|
||||||
torrentPlugin: null,
|
torrentPlugin: null,
|
||||||
torrentPlugins: [
|
torrentPlugins: [
|
||||||
'torrent',
|
'torrent',
|
||||||
'rtorrent',
|
'rtorrent',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
'file': true,
|
||||||
|
'youtube': true,
|
||||||
|
'torrent': true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -277,6 +308,20 @@ export default {
|
||||||
this.selectedSubtitles = null
|
this.selectedSubtitles = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async playUrl(url) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.play({
|
||||||
|
url: url,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$refs.playUrlModal.close()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -303,6 +348,9 @@ export default {
|
||||||
'platypush.message.event.torrent.TorrentDownloadStartEvent')
|
'platypush.message.event.torrent.TorrentDownloadStartEvent')
|
||||||
this.subscribe(this.onTorrentDownloadCompleted,'notify-on-torrent-download-completed',
|
this.subscribe(this.onTorrentDownloadCompleted,'notify-on-torrent-download-completed',
|
||||||
'platypush.message.event.torrent.TorrentDownloadCompletedEvent')
|
'platypush.message.event.torrent.TorrentDownloadCompletedEvent')
|
||||||
|
|
||||||
|
if ('media.plex' in this.$root.config)
|
||||||
|
this.sources.plex = true
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -316,6 +364,7 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "vars";
|
@import "vars";
|
||||||
|
@import "~@/components/Media/vars";
|
||||||
|
|
||||||
.media-plugin {
|
.media-plugin {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -335,8 +384,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-container {
|
.body-container {
|
||||||
height: calc(100% - #{$media-header-height});
|
height: calc(100% - #{$media-header-height} - #{$media-ctrl-panel-height});
|
||||||
margin-top: .2em;
|
padding-top: .1em;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&.expanded-header {
|
||||||
|
height: calc(100% - #{$media-header-height} - #{$filter-header-height} - #{$media-ctrl-panel-height});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,4 +421,38 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.play-url-container) {
|
||||||
|
.body {
|
||||||
|
padding: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=submit] {
|
||||||
|
background: initial;
|
||||||
|
border-color: initial;
|
||||||
|
border-radius: 1.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $default-hover-fg-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,21 @@
|
||||||
<div class="right side" v-text="item.description" />
|
<div class="right side" v-text="item.description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item?.summary">
|
||||||
|
<div class="left side">Summary</div>
|
||||||
|
<div class="right side" v-text="item.summary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item?.duration">
|
||||||
|
<div class="left side">Duration</div>
|
||||||
|
<div class="right side" v-text="convertTime(item.duration)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="item?.genres">
|
||||||
|
<div class="left side">Genres</div>
|
||||||
|
<div class="right side" v-text="item.genres.join(', ')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="item?.channelId">
|
<div class="row" v-if="item?.channelId">
|
||||||
<div class="left side">Channel</div>
|
<div class="left side">Channel</div>
|
||||||
<div class="right side">
|
<div class="right side">
|
||||||
|
@ -83,10 +98,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
|
import MediaUtils from "@/components/Media/Utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Info",
|
name: "Info",
|
||||||
mixins: [Utils],
|
mixins: [Utils, MediaUtils],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
@ -30,9 +30,9 @@ export default {
|
||||||
displayName: 'Search',
|
displayName: 'Search',
|
||||||
},
|
},
|
||||||
|
|
||||||
browse: {
|
browser: {
|
||||||
iconClass: 'fa fa-folder',
|
iconClass: 'fa fa-folder',
|
||||||
displayName: 'Browse',
|
displayName: 'Browser',
|
||||||
},
|
},
|
||||||
|
|
||||||
torrents: {
|
torrents: {
|
||||||
|
|
|
@ -93,7 +93,8 @@ export default {
|
||||||
|
|
||||||
supports(resource) {
|
supports(resource) {
|
||||||
return resource?.type === 'file' || resource?.type === 'youtube' ||
|
return resource?.type === 'file' || resource?.type === 'youtube' ||
|
||||||
(resource.url || resource).startsWith('http://') || (resource.url || resource).startsWith('https://')
|
(resource.url || resource).startsWith('file://') || (resource.url || resource).startsWith('http://') ||
|
||||||
|
(resource.url || resource).startsWith('https://')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-results">
|
<div class="media-results">
|
||||||
<div class="row item" :class="{selected: selectedResult === i}" v-for="(result, i) in results" :key="i"
|
<div class="no-content" v-if="!results?.length">
|
||||||
@click="$emit('select', i)">
|
No search results
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row item" :class="{selected: selectedResult === i, hidden: !sources[result.type]}"
|
||||||
|
v-for="(result, i) in results" :key="i" @click="$emit('select', i)">
|
||||||
<div class="col-10 left side">
|
<div class="col-10 left side">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<i :class="typeIcons[result.type]" />
|
<i :class="typeIcons[result.type]" />
|
||||||
|
@ -41,6 +45,11 @@ export default {
|
||||||
selectedResult: {
|
selectedResult: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sources: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -49,6 +58,7 @@ export default {
|
||||||
'file': 'fa fa-hdd',
|
'file': 'fa fa-hdd',
|
||||||
'torrent': 'fa fa-magnet',
|
'torrent': 'fa fa-magnet',
|
||||||
'youtube': 'fab fa-youtube',
|
'youtube': 'fab fa-youtube',
|
||||||
|
'plex': 'fa fa-plex',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -98,5 +108,15 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
.fa-youtube {
|
||||||
|
color: #d21;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
.fa.fa-kodi:before {
|
@mixin icon {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
background: url('/icons/kodi.svg');
|
|
||||||
background-size: 1em 1em;
|
background-size: 1em 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa.fa-kodi:before {
|
||||||
|
@include icon;
|
||||||
|
background: url('/icons/kodi.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa.fa-plex:before {
|
||||||
|
@include icon;
|
||||||
|
background: url('/icons/plex.svg');
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
@ -158,5 +159,36 @@ class FilePlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
pathlib.Path(self._get_path(file)).unlink()
|
pathlib.Path(self._get_path(file)).unlink()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def list(self, path: str = os.path.abspath(os.sep)) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
List a file or all the files in a directory.
|
||||||
|
|
||||||
|
:param path: File or directory (default: root directory).
|
||||||
|
:return: List of files in the specified path, or absolute path of the specified path if ``path`` is a file and
|
||||||
|
it exists. Each item will contain the fields ``type`` (``file`` or ``directory``) and ``path``.
|
||||||
|
"""
|
||||||
|
path = self._get_path(path)
|
||||||
|
assert os.path.exists(path), 'No such file or directory: {}'.format(path)
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
return [{
|
||||||
|
'type': 'file',
|
||||||
|
'path': path,
|
||||||
|
'name': os.path.basename(path),
|
||||||
|
}]
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'type': 'directory' if os.path.isdir(os.path.join(path, f)) else 'file',
|
||||||
|
'path': os.path.join(path, f),
|
||||||
|
'name': os.path.basename(f),
|
||||||
|
}
|
||||||
|
for f in sorted(os.listdir(path))
|
||||||
|
],
|
||||||
|
key=lambda f: (f.get('type'), f.get('name'))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -66,7 +66,7 @@ class MediaPlugin(Plugin):
|
||||||
_supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv',
|
_supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv',
|
||||||
'media.vlc', 'media.chromecast', 'media.gstreamer'}
|
'media.vlc', 'media.chromecast', 'media.gstreamer'}
|
||||||
|
|
||||||
_supported_media_types = ['file', 'torrent', 'youtube']
|
_supported_media_types = ['file', 'plex', 'torrent', 'youtube']
|
||||||
_default_search_timeout = 60 # 60 seconds
|
_default_search_timeout = 60 # 60 seconds
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
@ -202,6 +202,8 @@ class MediaPlugin(Plugin):
|
||||||
resource = self._videos_queue.pop(0)
|
resource = self._videos_queue.pop(0)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('No media file found in torrent {}'.format(resource))
|
raise RuntimeError('No media file found in torrent {}'.format(resource))
|
||||||
|
elif re.search(r'^https?://', resource):
|
||||||
|
return resource
|
||||||
|
|
||||||
assert resource, 'Unable to find any compatible media resource'
|
assert resource, 'Unable to find any compatible media resource'
|
||||||
return resource
|
return resource
|
||||||
|
@ -377,6 +379,9 @@ class MediaPlugin(Plugin):
|
||||||
if search_type == 'youtube':
|
if search_type == 'youtube':
|
||||||
from .search import YoutubeMediaSearcher
|
from .search import YoutubeMediaSearcher
|
||||||
return YoutubeMediaSearcher()
|
return YoutubeMediaSearcher()
|
||||||
|
if search_type == 'plex':
|
||||||
|
from .search import PlexMediaSearcher
|
||||||
|
return PlexMediaSearcher()
|
||||||
|
|
||||||
self.logger.warning('Unsupported search type: {}'.format(search_type))
|
self.logger.warning('Unsupported search type: {}'.format(search_type))
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
self._post_event(MediaPauseEvent, resource=self._get_current_resource(), title=self._player.filename)
|
self._post_event(MediaPauseEvent, resource=self._get_current_resource(), title=self._player.filename)
|
||||||
elif evt == Event.UNPAUSE:
|
elif evt == Event.UNPAUSE:
|
||||||
self._post_event(MediaPlayEvent, resource=self._get_current_resource(), title=self._player.filename)
|
self._post_event(MediaPlayEvent, resource=self._get_current_resource(), title=self._player.filename)
|
||||||
elif evt == Event.SHUTDOWN or (
|
elif evt == Event.SHUTDOWN or evt == Event.IDLE or (
|
||||||
evt == Event.END_FILE and event.get('event', {}).get('reason') in
|
evt == Event.END_FILE and event.get('event', {}).get('reason') in
|
||||||
[EndFile.EOF, EndFile.ABORTED, EndFile.QUIT]):
|
[EndFile.EOF, EndFile.ABORTED, EndFile.QUIT]):
|
||||||
playback_rebounced = self._playback_rebounce_event.wait(timeout=0.5)
|
playback_rebounced = self._playback_rebounce_event.wait(timeout=0.5)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
@ -11,7 +13,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
* **plexapi** (``pip install plexapi``)
|
* **plexapi** (``pip install plexapi``)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server, username, password, *args, **kwargs):
|
def __init__(self, server, username, password, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param server: Plex server name
|
:param server: Plex server name
|
||||||
:type server: str
|
:type server: str
|
||||||
|
@ -24,12 +26,11 @@ class MediaPlexPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.resource = MyPlexAccount(username, password).resource(server)
|
self.resource = MyPlexAccount(username, password).resource(server)
|
||||||
self._plex = None
|
self._plex = None
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plex(self):
|
def plex(self):
|
||||||
if not self._plex:
|
if not self._plex:
|
||||||
|
@ -37,7 +38,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self._plex
|
return self._plex
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_clients(self):
|
def get_clients(self):
|
||||||
"""
|
"""
|
||||||
|
@ -57,11 +57,9 @@ class MediaPlexPlugin(Plugin):
|
||||||
'version': c.version,
|
'version': c.version,
|
||||||
} for c in self.plex.clients()]
|
} for c in self.plex.clients()]
|
||||||
|
|
||||||
|
|
||||||
def _get_client(self, name):
|
def _get_client(self, name):
|
||||||
return self.plex.client(name)
|
return self.plex.client(name)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def search(self, section=None, title=None, **kwargs):
|
def search(self, section=None, title=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -93,7 +91,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlists(self):
|
def playlists(self):
|
||||||
"""
|
"""
|
||||||
|
@ -106,11 +103,10 @@ class MediaPlexPlugin(Plugin):
|
||||||
'duration': pl.duration,
|
'duration': pl.duration,
|
||||||
'summary': pl.summary,
|
'summary': pl.summary,
|
||||||
'viewed_at': pl.viewedAt,
|
'viewed_at': pl.viewedAt,
|
||||||
'items': [ self._flatten_item(item) for item in pl.items() ],
|
'items': [self._flatten_item(item) for item in pl.items()],
|
||||||
} for pl in self.plex.playlists()
|
} for pl in self.plex.playlists()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def history(self):
|
def history(self):
|
||||||
"""
|
"""
|
||||||
|
@ -121,8 +117,8 @@ class MediaPlexPlugin(Plugin):
|
||||||
self._flatten_item(item) for item in self.plex.history()
|
self._flatten_item(item) for item in self.plex.history()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def get_chromecast(self, chromecast):
|
def get_chromecast(chromecast):
|
||||||
from .lib.plexcast import PlexController
|
from .lib.plexcast import PlexController
|
||||||
|
|
||||||
hndl = PlexController()
|
hndl = PlexController()
|
||||||
|
@ -130,8 +126,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
cast = get_plugin('media.chromecast').get_chromecast(chromecast)
|
cast = get_plugin('media.chromecast').get_chromecast(chromecast)
|
||||||
cast.register_handler(hndl)
|
cast.register_handler(hndl)
|
||||||
|
|
||||||
return (cast, hndl)
|
return cast, hndl
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, client=None, chromecast=None, **kwargs):
|
def play(self, client=None, chromecast=None, **kwargs):
|
||||||
|
@ -157,7 +152,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
raise RuntimeError('No client nor chromecast specified')
|
raise RuntimeError('No client nor chromecast specified')
|
||||||
|
|
||||||
if client:
|
if client:
|
||||||
client = plex.client(client)
|
client = self.plex.client(client)
|
||||||
elif chromecast:
|
elif chromecast:
|
||||||
(chromecast, handler) = self.get_chromecast(chromecast)
|
(chromecast, handler) = self.get_chromecast(chromecast)
|
||||||
|
|
||||||
|
@ -185,7 +180,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
elif chromecast:
|
elif chromecast:
|
||||||
return handler.play_media(item, self.plex)
|
return handler.play_media(item, self.plex)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self, client):
|
def pause(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -194,7 +188,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).pause()
|
return self.client(client).pause()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self, client):
|
def stop(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -203,7 +196,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).stop()
|
return self.client(client).stop()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, client, offset):
|
def seek(self, client, offset):
|
||||||
"""
|
"""
|
||||||
|
@ -212,7 +204,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).seekTo(offset)
|
return self.client(client).seekTo(offset)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self, client):
|
def forward(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -221,7 +212,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).stepForward()
|
return self.client(client).stepForward()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self, client):
|
def back(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -230,7 +220,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).stepBack()
|
return self.client(client).stepBack()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def next(self, client):
|
def next(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -239,7 +228,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).skipNext()
|
return self.client(client).skipNext()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def previous(self, client):
|
def previous(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -248,15 +236,13 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).skipPrevious()
|
return self.client(client).skipPrevious()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, client, volume):
|
def set_volume(self, client, volume):
|
||||||
"""
|
"""
|
||||||
Set the volume on a client between 0 and 100
|
Set the volume on a client between 0 and 100
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.client(client).setVolume(volume/100)
|
return self.client(client).setVolume(volume / 100)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def repeat(self, client, repeat):
|
def repeat(self, client, repeat):
|
||||||
|
@ -266,7 +252,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).setRepeat(repeat)
|
return self.client(client).setRepeat(repeat)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def random(self, client, random):
|
def random(self, client, random):
|
||||||
"""
|
"""
|
||||||
|
@ -275,7 +260,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).setShuffle(random)
|
return self.client(client).setShuffle(random)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def up(self, client):
|
def up(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -284,7 +268,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).moveUp()
|
return self.client(client).moveUp()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def down(self, client):
|
def down(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -293,7 +276,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).moveDown()
|
return self.client(client).moveDown()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def left(self, client):
|
def left(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -302,7 +284,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).moveLeft()
|
return self.client(client).moveLeft()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def right(self, client):
|
def right(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -311,7 +292,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).moveRight()
|
return self.client(client).moveRight()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def go_back(self, client):
|
def go_back(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -320,7 +300,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).goBack()
|
return self.client(client).goBack()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def go_home(self, client):
|
def go_home(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -329,7 +308,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).goHome()
|
return self.client(client).goHome()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def go_to_media(self, client):
|
def go_to_media(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -338,7 +316,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).goToMedia()
|
return self.client(client).goToMedia()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def go_to_music(self, client):
|
def go_to_music(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -347,7 +324,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).goToMusic()
|
return self.client(client).goToMusic()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def next_letter(self, client):
|
def next_letter(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -356,7 +332,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).nextLetter()
|
return self.client(client).nextLetter()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def page_down(self, client):
|
def page_down(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -365,7 +340,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).pageDown()
|
return self.client(client).pageDown()
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def page_up(self, client):
|
def page_up(self, client):
|
||||||
"""
|
"""
|
||||||
|
@ -374,7 +348,6 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
return self.client(client).pageUp()
|
return self.client(client).pageUp()
|
||||||
|
|
||||||
|
|
||||||
def _flatten_item(self, item):
|
def _flatten_item(self, item):
|
||||||
from plexapi.video import Movie, Show
|
from plexapi.video import Movie, Show
|
||||||
|
|
||||||
|
@ -399,7 +372,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
|
|
||||||
_item['media'] = [
|
_item['media'] = [
|
||||||
{
|
{
|
||||||
'duration': (item.media[i].duration or 0)/1000,
|
'duration': (item.media[i].duration or 0) / 1000,
|
||||||
'width': item.media[i].width,
|
'width': item.media[i].width,
|
||||||
'height': item.media[i].height,
|
'height': item.media[i].height,
|
||||||
'audio_channels': item.media[i].audioChannels,
|
'audio_channels': item.media[i].audioChannels,
|
||||||
|
@ -410,7 +383,10 @@ class MediaPlexPlugin(Plugin):
|
||||||
'parts': [
|
'parts': [
|
||||||
{
|
{
|
||||||
'file': part.file,
|
'file': part.file,
|
||||||
'duration': (part.duration or 0)/1000,
|
'duration': (part.duration or 0) / 1000,
|
||||||
|
'url': self.plex.url(part.key) + '?' + urllib.parse.urlencode({
|
||||||
|
'X-Plex-Token': self.plex._token,
|
||||||
|
}),
|
||||||
} for part in item.media[i].parts
|
} for part in item.media[i].parts
|
||||||
]
|
]
|
||||||
} for i in range(0, len(item.media))
|
} for i in range(0, len(item.media))
|
||||||
|
@ -424,7 +400,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
'summary': season.summary,
|
'summary': season.summary,
|
||||||
'episodes': [
|
'episodes': [
|
||||||
{
|
{
|
||||||
'duration': episode.duration/1000,
|
'duration': episode.duration / 1000,
|
||||||
'index': episode.index,
|
'index': episode.index,
|
||||||
'year': episode.year,
|
'year': episode.year,
|
||||||
'season_number': episode.seasonNumber,
|
'season_number': episode.seasonNumber,
|
||||||
|
@ -435,7 +411,7 @@ class MediaPlexPlugin(Plugin):
|
||||||
'view_offset': episode.viewOffset,
|
'view_offset': episode.viewOffset,
|
||||||
'media': [
|
'media': [
|
||||||
{
|
{
|
||||||
'duration': episode.media[i].duration/1000,
|
'duration': episode.media[i].duration / 1000,
|
||||||
'width': episode.media[i].width,
|
'width': episode.media[i].width,
|
||||||
'height': episode.media[i].height,
|
'height': episode.media[i].height,
|
||||||
'audio_channels': episode.media[i].audioChannels,
|
'audio_channels': episode.media[i].audioChannels,
|
||||||
|
@ -447,7 +423,10 @@ class MediaPlexPlugin(Plugin):
|
||||||
'parts': [
|
'parts': [
|
||||||
{
|
{
|
||||||
'file': part.file,
|
'file': part.file,
|
||||||
'duration': part.duration/1000,
|
'duration': part.duration / 1000,
|
||||||
|
'url': self.plex.url(part.key) + '?' + urllib.parse.urlencode({
|
||||||
|
'X-Plex-Token': self.plex._token,
|
||||||
|
}),
|
||||||
} for part in episode.media[i].parts
|
} for part in episode.media[i].parts
|
||||||
]
|
]
|
||||||
} for i in range(0, len(episode.locations))
|
} for i in range(0, len(episode.locations))
|
||||||
|
@ -460,5 +439,4 @@ class MediaPlexPlugin(Plugin):
|
||||||
return _item
|
return _item
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -17,8 +17,9 @@ class MediaSearcher:
|
||||||
from .local import LocalMediaSearcher
|
from .local import LocalMediaSearcher
|
||||||
from .youtube import YoutubeMediaSearcher
|
from .youtube import YoutubeMediaSearcher
|
||||||
from .torrent import TorrentMediaSearcher
|
from .torrent import TorrentMediaSearcher
|
||||||
|
from .plex import PlexMediaSearcher
|
||||||
|
|
||||||
__all__ = ['LocalMediaSearcher', 'TorrentMediaSearcher', 'YoutubeMediaSearcher']
|
__all__ = ['MediaSearcher', 'LocalMediaSearcher', 'TorrentMediaSearcher', 'YoutubeMediaSearcher', 'PlexMediaSearcher']
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
61
platypush/plugins/media/search/plex.py
Normal file
61
platypush/plugins/media/search/plex.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
from platypush.context import get_plugin
|
||||||
|
from platypush.plugins.media.search import MediaSearcher
|
||||||
|
|
||||||
|
|
||||||
|
class PlexMediaSearcher(MediaSearcher):
|
||||||
|
def search(self, query, **kwargs):
|
||||||
|
"""
|
||||||
|
Performs a Plex search using the configured :class:`platypush.plugins.media.plex.MediaPlexPlugin` instance if
|
||||||
|
it is available.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
plex = get_plugin('media.plex')
|
||||||
|
except RuntimeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
self.logger.info('Searching Plex for "{}"'.format(query))
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for result in plex.search(title=query).output:
|
||||||
|
results.extend(self._flatten_result(result))
|
||||||
|
|
||||||
|
self.logger.info('{} Plex results found for the search query "{}"'.format(len(results), query))
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _flatten_result(result):
|
||||||
|
def parse_part(media, part, episode=None, sub_media=None):
|
||||||
|
if 'episodes' in media:
|
||||||
|
del media['episodes']
|
||||||
|
|
||||||
|
return {
|
||||||
|
**{k: v for k, v in result.items() if k not in ['media', 'type']},
|
||||||
|
'media_type': result.get('type'),
|
||||||
|
'type': 'plex',
|
||||||
|
**{k: v for k, v in media.items() if k not in ['parts']},
|
||||||
|
**part,
|
||||||
|
'title': '{}{}{}'.format(
|
||||||
|
result.get('title', ''),
|
||||||
|
' [{}]'.format(episode['season_episode']) if (episode or {}).get('season_episode') else '',
|
||||||
|
' {}'.format(sub_media['title']) if (sub_media or {}).get('title') else '',
|
||||||
|
),
|
||||||
|
'summary': episode['summary'] if (episode or {}).get('summary') else media.get('summary'),
|
||||||
|
}
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for media in result.get('media', []):
|
||||||
|
if 'episodes' in media:
|
||||||
|
for episode in media['episodes']:
|
||||||
|
for sub_media in episode.get('media', []):
|
||||||
|
for part in sub_media.get('parts', []):
|
||||||
|
results.append(parse_part(media=media, episode=episode, sub_media=sub_media, part=part))
|
||||||
|
else:
|
||||||
|
for part in media.get('parts', []):
|
||||||
|
results.append(parse_part(media=media, part=part))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
Loading…
Reference in a new issue