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;
|
||||
|
||||
.view-container {
|
||||
height: calc(100% - #{$media-ctrl-panel-height});
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
|
|
|
@ -52,9 +52,10 @@ export default {
|
|||
|
||||
let element = event.target
|
||||
while (element) {
|
||||
if (element === this.$refs.dropdown.element) {
|
||||
if (!this.$refs.dropdown)
|
||||
break
|
||||
if (element === this.$refs.dropdown.element)
|
||||
return
|
||||
}
|
||||
|
||||
element = element.parentElement
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<div class="header" :class="{'with-filter': filterVisible}">
|
||||
<div class="row">
|
||||
<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" />
|
||||
</button>
|
||||
|
||||
|
@ -21,6 +22,13 @@
|
|||
</form>
|
||||
</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">
|
||||
<button title="Select subtitles" class="captions-btn" :class="{selected: selectedSubtitles != null}"
|
||||
@click="$emit('show-subtitles')" v-if="hasSubtitlesPlugin && selectedItem &&
|
||||
|
@ -30,15 +38,16 @@
|
|||
|
||||
<Players :plugin-name="pluginName" @select="$emit('select-player', $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" />
|
||||
</Dropdown>
|
||||
|
||||
<button title="Play URL" @click="$emit('play-url')">
|
||||
<i class="fa fa-plus-circle" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row filter fade-in" :class="{hidden: !filterVisible}">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -46,13 +55,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "@/components/elements/Dropdown";
|
||||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
import Players from "@/components/panels/Media/Players";
|
||||
export default {
|
||||
name: "Header",
|
||||
components: {Players, DropdownItem, Dropdown},
|
||||
emits: ['search', 'select-player', 'player-status', 'torrent-add', 'show-subtitles'],
|
||||
components: {Players},
|
||||
emits: ['search', 'select-player', 'player-status', 'torrent-add', 'show-subtitles', 'play-url', 'filter',
|
||||
'source-toggle'],
|
||||
|
||||
props: {
|
||||
pluginName: {
|
||||
|
@ -77,6 +85,16 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
browserFilter: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
sources: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -84,11 +102,6 @@ export default {
|
|||
filterVisible: false,
|
||||
query: '',
|
||||
torrentURL: '',
|
||||
sources: {
|
||||
'file': true,
|
||||
'youtube': true,
|
||||
'torrent': true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -103,7 +116,15 @@ export default {
|
|||
types: types,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$watch(() => this.selectedView, () => {
|
||||
this.$emit('filter', '')
|
||||
this.torrentURL = ''
|
||||
this.query = ''
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -118,6 +139,10 @@ export default {
|
|||
padding: .5em;
|
||||
box-shadow: $border-shadow-bottom;
|
||||
|
||||
.filter-btn.selected {
|
||||
color: $selected-fg;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -125,6 +150,7 @@ export default {
|
|||
|
||||
&.with-filter {
|
||||
height: calc(#{$media-header-height} + #{$filter-header-height});
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.side {
|
||||
|
@ -166,10 +192,9 @@ export default {
|
|||
}
|
||||
|
||||
.filter {
|
||||
position: absolute;
|
||||
top: $media-header-height;
|
||||
width: 100%;
|
||||
height: $filter-header-height;
|
||||
padding-bottom: 1em;
|
||||
margin-top: .5em;
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
|
|
|
@ -13,19 +13,24 @@
|
|||
|
||||
<div class="view-container">
|
||||
<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 : results[selectedResult]" :selected-subtitles="selectedSubtitles"
|
||||
@search="search" @select-player="selectedPlayer = $event" @player-status="onStatusUpdate"
|
||||
@torrent-add="downloadTorrent($event)" @show-subtitles="showSubtitlesModal = !showSubtitlesModal" />
|
||||
:browser-filter="browserFilter" @search="search" @select-player="selectedPlayer = $event"
|
||||
@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)"
|
||||
@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"
|
||||
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>
|
||||
</main>
|
||||
|
@ -47,6 +52,23 @@
|
|||
</div>
|
||||
</Modal>
|
||||
</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>
|
||||
</keep-alive>
|
||||
</template>
|
||||
|
@ -63,11 +85,12 @@ import Nav from "@/components/panels/Media/Nav";
|
|||
import Results from "@/components/panels/Media/Results";
|
||||
import Subtitles from "@/components/panels/Media/Subtitles";
|
||||
import TorrentView from "@/components/panels/Torrent/View";
|
||||
import Browser from "@/components/File/Browser";
|
||||
|
||||
export default {
|
||||
name: "Media",
|
||||
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: {
|
||||
pluginName: {
|
||||
type: String,
|
||||
|
@ -96,11 +119,19 @@ export default {
|
|||
selectedSubtitles: null,
|
||||
showSubtitlesModal: false,
|
||||
awaitingPlayTorrent: null,
|
||||
urlPlay: null,
|
||||
browserFilter: null,
|
||||
torrentPlugin: null,
|
||||
torrentPlugins: [
|
||||
'torrent',
|
||||
'rtorrent',
|
||||
],
|
||||
|
||||
sources: {
|
||||
'file': true,
|
||||
'youtube': true,
|
||||
'torrent': true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -277,6 +308,20 @@ export default {
|
|||
this.selectedSubtitles = null
|
||||
}
|
||||
},
|
||||
|
||||
async playUrl(url) {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.play({
|
||||
url: url,
|
||||
})
|
||||
|
||||
this.$refs.playUrlModal.close()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -303,6 +348,9 @@ export default {
|
|||
'platypush.message.event.torrent.TorrentDownloadStartEvent')
|
||||
this.subscribe(this.onTorrentDownloadCompleted,'notify-on-torrent-download-completed',
|
||||
'platypush.message.event.torrent.TorrentDownloadCompletedEvent')
|
||||
|
||||
if ('media.plex' in this.$root.config)
|
||||
this.sources.plex = true
|
||||
},
|
||||
|
||||
destroy() {
|
||||
|
@ -316,6 +364,7 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
@import "vars";
|
||||
@import "~@/components/Media/vars";
|
||||
|
||||
.media-plugin {
|
||||
width: 100%;
|
||||
|
@ -335,8 +384,13 @@ export default {
|
|||
}
|
||||
|
||||
.body-container {
|
||||
height: calc(100% - #{$media-header-height});
|
||||
margin-top: .2em;
|
||||
height: calc(100% - #{$media-header-height} - #{$media-ctrl-panel-height});
|
||||
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>
|
||||
|
|
|
@ -18,6 +18,21 @@
|
|||
<div class="right side" v-text="item.description" />
|
||||
</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="left side">Channel</div>
|
||||
<div class="right side">
|
||||
|
@ -83,10 +98,11 @@
|
|||
|
||||
<script>
|
||||
import Utils from "@/Utils";
|
||||
import MediaUtils from "@/components/Media/Utils";
|
||||
|
||||
export default {
|
||||
name: "Info",
|
||||
mixins: [Utils],
|
||||
mixins: [Utils, MediaUtils],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
|
|
@ -30,9 +30,9 @@ export default {
|
|||
displayName: 'Search',
|
||||
},
|
||||
|
||||
browse: {
|
||||
browser: {
|
||||
iconClass: 'fa fa-folder',
|
||||
displayName: 'Browse',
|
||||
displayName: 'Browser',
|
||||
},
|
||||
|
||||
torrents: {
|
||||
|
|
|
@ -93,7 +93,8 @@ export default {
|
|||
|
||||
supports(resource) {
|
||||
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>
|
||||
<div class="media-results">
|
||||
<div class="row item" :class="{selected: selectedResult === i}" v-for="(result, i) in results" :key="i"
|
||||
@click="$emit('select', i)">
|
||||
<div class="no-content" v-if="!results?.length">
|
||||
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="icon">
|
||||
<i :class="typeIcons[result.type]" />
|
||||
|
@ -41,6 +45,11 @@ export default {
|
|||
selectedResult: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
sources: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -49,6 +58,7 @@ export default {
|
|||
'file': 'fa fa-hdd',
|
||||
'torrent': 'fa fa-magnet',
|
||||
'youtube': 'fab fa-youtube',
|
||||
'plex': 'fa fa-plex',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -98,5 +108,15 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.fa-youtube {
|
||||
color: #d21;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
.fa.fa-kodi:before {
|
||||
@mixin icon {
|
||||
content: ' ';
|
||||
background: url('/icons/kodi.svg');
|
||||
background-size: 1em 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
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 os
|
||||
import pathlib
|
||||
from typing import List, Dict
|
||||
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
@ -158,5 +159,36 @@ class FilePlugin(Plugin):
|
|||
"""
|
||||
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:
|
||||
|
|
|
@ -66,7 +66,7 @@ class MediaPlugin(Plugin):
|
|||
_supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv',
|
||||
'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
|
||||
|
||||
def __init__(self,
|
||||
|
@ -202,6 +202,8 @@ class MediaPlugin(Plugin):
|
|||
resource = self._videos_queue.pop(0)
|
||||
else:
|
||||
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'
|
||||
return resource
|
||||
|
@ -377,6 +379,9 @@ class MediaPlugin(Plugin):
|
|||
if search_type == 'youtube':
|
||||
from .search import YoutubeMediaSearcher
|
||||
return YoutubeMediaSearcher()
|
||||
if search_type == 'plex':
|
||||
from .search import PlexMediaSearcher
|
||||
return PlexMediaSearcher()
|
||||
|
||||
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)
|
||||
elif evt == Event.UNPAUSE:
|
||||
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
|
||||
[EndFile.EOF, EndFile.ABORTED, EndFile.QUIT]):
|
||||
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.plugins import Plugin, action
|
||||
|
||||
|
@ -11,7 +13,7 @@ class MediaPlexPlugin(Plugin):
|
|||
* **plexapi** (``pip install plexapi``)
|
||||
"""
|
||||
|
||||
def __init__(self, server, username, password, *args, **kwargs):
|
||||
def __init__(self, server, username, password, **kwargs):
|
||||
"""
|
||||
:param server: Plex server name
|
||||
:type server: str
|
||||
|
@ -24,12 +26,11 @@ class MediaPlexPlugin(Plugin):
|
|||
"""
|
||||
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.resource = MyPlexAccount(username, password).resource(server)
|
||||
self._plex = None
|
||||
|
||||
|
||||
@property
|
||||
def plex(self):
|
||||
if not self._plex:
|
||||
|
@ -37,7 +38,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self._plex
|
||||
|
||||
|
||||
@action
|
||||
def get_clients(self):
|
||||
"""
|
||||
|
@ -57,11 +57,9 @@ class MediaPlexPlugin(Plugin):
|
|||
'version': c.version,
|
||||
} for c in self.plex.clients()]
|
||||
|
||||
|
||||
def _get_client(self, name):
|
||||
return self.plex.client(name)
|
||||
|
||||
|
||||
@action
|
||||
def search(self, section=None, title=None, **kwargs):
|
||||
"""
|
||||
|
@ -93,7 +91,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return ret
|
||||
|
||||
|
||||
@action
|
||||
def playlists(self):
|
||||
"""
|
||||
|
@ -106,11 +103,10 @@ class MediaPlexPlugin(Plugin):
|
|||
'duration': pl.duration,
|
||||
'summary': pl.summary,
|
||||
'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()
|
||||
]
|
||||
|
||||
|
||||
@action
|
||||
def history(self):
|
||||
"""
|
||||
|
@ -121,8 +117,8 @@ class MediaPlexPlugin(Plugin):
|
|||
self._flatten_item(item) for item in self.plex.history()
|
||||
]
|
||||
|
||||
|
||||
def get_chromecast(self, chromecast):
|
||||
@staticmethod
|
||||
def get_chromecast(chromecast):
|
||||
from .lib.plexcast import PlexController
|
||||
|
||||
hndl = PlexController()
|
||||
|
@ -130,8 +126,7 @@ class MediaPlexPlugin(Plugin):
|
|||
cast = get_plugin('media.chromecast').get_chromecast(chromecast)
|
||||
cast.register_handler(hndl)
|
||||
|
||||
return (cast, hndl)
|
||||
|
||||
return cast, hndl
|
||||
|
||||
@action
|
||||
def play(self, client=None, chromecast=None, **kwargs):
|
||||
|
@ -157,7 +152,7 @@ class MediaPlexPlugin(Plugin):
|
|||
raise RuntimeError('No client nor chromecast specified')
|
||||
|
||||
if client:
|
||||
client = plex.client(client)
|
||||
client = self.plex.client(client)
|
||||
elif chromecast:
|
||||
(chromecast, handler) = self.get_chromecast(chromecast)
|
||||
|
||||
|
@ -185,7 +180,6 @@ class MediaPlexPlugin(Plugin):
|
|||
elif chromecast:
|
||||
return handler.play_media(item, self.plex)
|
||||
|
||||
|
||||
@action
|
||||
def pause(self, client):
|
||||
"""
|
||||
|
@ -194,7 +188,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).pause()
|
||||
|
||||
|
||||
@action
|
||||
def stop(self, client):
|
||||
"""
|
||||
|
@ -203,7 +196,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).stop()
|
||||
|
||||
|
||||
@action
|
||||
def seek(self, client, offset):
|
||||
"""
|
||||
|
@ -212,7 +204,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).seekTo(offset)
|
||||
|
||||
|
||||
@action
|
||||
def forward(self, client):
|
||||
"""
|
||||
|
@ -221,7 +212,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).stepForward()
|
||||
|
||||
|
||||
@action
|
||||
def back(self, client):
|
||||
"""
|
||||
|
@ -230,7 +220,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).stepBack()
|
||||
|
||||
|
||||
@action
|
||||
def next(self, client):
|
||||
"""
|
||||
|
@ -239,7 +228,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).skipNext()
|
||||
|
||||
|
||||
@action
|
||||
def previous(self, client):
|
||||
"""
|
||||
|
@ -248,15 +236,13 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).skipPrevious()
|
||||
|
||||
|
||||
@action
|
||||
def set_volume(self, client, volume):
|
||||
"""
|
||||
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
|
||||
def repeat(self, client, repeat):
|
||||
|
@ -266,7 +252,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).setRepeat(repeat)
|
||||
|
||||
|
||||
@action
|
||||
def random(self, client, random):
|
||||
"""
|
||||
|
@ -275,7 +260,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).setShuffle(random)
|
||||
|
||||
|
||||
@action
|
||||
def up(self, client):
|
||||
"""
|
||||
|
@ -284,7 +268,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).moveUp()
|
||||
|
||||
|
||||
@action
|
||||
def down(self, client):
|
||||
"""
|
||||
|
@ -293,7 +276,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).moveDown()
|
||||
|
||||
|
||||
@action
|
||||
def left(self, client):
|
||||
"""
|
||||
|
@ -302,7 +284,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).moveLeft()
|
||||
|
||||
|
||||
@action
|
||||
def right(self, client):
|
||||
"""
|
||||
|
@ -311,7 +292,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).moveRight()
|
||||
|
||||
|
||||
@action
|
||||
def go_back(self, client):
|
||||
"""
|
||||
|
@ -320,7 +300,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).goBack()
|
||||
|
||||
|
||||
@action
|
||||
def go_home(self, client):
|
||||
"""
|
||||
|
@ -329,7 +308,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).goHome()
|
||||
|
||||
|
||||
@action
|
||||
def go_to_media(self, client):
|
||||
"""
|
||||
|
@ -338,7 +316,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).goToMedia()
|
||||
|
||||
|
||||
@action
|
||||
def go_to_music(self, client):
|
||||
"""
|
||||
|
@ -347,7 +324,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).goToMusic()
|
||||
|
||||
|
||||
@action
|
||||
def next_letter(self, client):
|
||||
"""
|
||||
|
@ -356,7 +332,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).nextLetter()
|
||||
|
||||
|
||||
@action
|
||||
def page_down(self, client):
|
||||
"""
|
||||
|
@ -365,7 +340,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).pageDown()
|
||||
|
||||
|
||||
@action
|
||||
def page_up(self, client):
|
||||
"""
|
||||
|
@ -374,7 +348,6 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
return self.client(client).pageUp()
|
||||
|
||||
|
||||
def _flatten_item(self, item):
|
||||
from plexapi.video import Movie, Show
|
||||
|
||||
|
@ -399,7 +372,7 @@ class MediaPlexPlugin(Plugin):
|
|||
|
||||
_item['media'] = [
|
||||
{
|
||||
'duration': (item.media[i].duration or 0)/1000,
|
||||
'duration': (item.media[i].duration or 0) / 1000,
|
||||
'width': item.media[i].width,
|
||||
'height': item.media[i].height,
|
||||
'audio_channels': item.media[i].audioChannels,
|
||||
|
@ -410,7 +383,10 @@ class MediaPlexPlugin(Plugin):
|
|||
'parts': [
|
||||
{
|
||||
'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 i in range(0, len(item.media))
|
||||
|
@ -424,7 +400,7 @@ class MediaPlexPlugin(Plugin):
|
|||
'summary': season.summary,
|
||||
'episodes': [
|
||||
{
|
||||
'duration': episode.duration/1000,
|
||||
'duration': episode.duration / 1000,
|
||||
'index': episode.index,
|
||||
'year': episode.year,
|
||||
'season_number': episode.seasonNumber,
|
||||
|
@ -435,7 +411,7 @@ class MediaPlexPlugin(Plugin):
|
|||
'view_offset': episode.viewOffset,
|
||||
'media': [
|
||||
{
|
||||
'duration': episode.media[i].duration/1000,
|
||||
'duration': episode.media[i].duration / 1000,
|
||||
'width': episode.media[i].width,
|
||||
'height': episode.media[i].height,
|
||||
'audio_channels': episode.media[i].audioChannels,
|
||||
|
@ -447,7 +423,10 @@ class MediaPlexPlugin(Plugin):
|
|||
'parts': [
|
||||
{
|
||||
'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 i in range(0, len(episode.locations))
|
||||
|
@ -460,5 +439,4 @@ class MediaPlexPlugin(Plugin):
|
|||
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 .youtube import YoutubeMediaSearcher
|
||||
from .torrent import TorrentMediaSearcher
|
||||
from .plex import PlexMediaSearcher
|
||||
|
||||
__all__ = ['LocalMediaSearcher', 'TorrentMediaSearcher', 'YoutubeMediaSearcher']
|
||||
__all__ = ['MediaSearcher', 'LocalMediaSearcher', 'TorrentMediaSearcher', 'YoutubeMediaSearcher', 'PlexMediaSearcher']
|
||||
|
||||
|
||||
# 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