[#408] Rewritten+expanded torrent UI.

Closes: #408
This commit is contained in:
Fabio Manganiello 2024-06-24 19:20:04 +02:00
parent 1774e464cc
commit 3dc1ff3c6e
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
10 changed files with 879 additions and 108 deletions

View file

@ -46,7 +46,7 @@
@download="download"
v-if="selectedView === 'search'" />
<TorrentView :plugin-name="torrentPlugin"
<Transfers :plugin-name="torrentPlugin"
:is-media="true"
@play="play"
v-else-if="selectedView === 'torrents'" />
@ -92,7 +92,7 @@ import MediaView from "@/components/Media/View";
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 Transfers from "@/components/panels/Torrent/Transfers";
import UrlPlayer from "@/components/panels/Media/UrlPlayer";
export default {
@ -107,7 +107,7 @@ export default {
Nav,
Results,
Subtitles,
TorrentView,
Transfers,
UrlPlayer,
},

View file

@ -1,27 +1,119 @@
<template>
<div class="header" :class="{'with-filter': filterVisible}">
<div class="header" :class="{'with-nav': withNav}">
<div class="row">
<div class="col-s-12 col-m-9 col-l-7 left side">
<form @submit.prevent="$emit('torrent-add', torrentURL)">
<div class="left side" :class="leftSideClasses">
<form @submit.prevent="submit">
<label class="search-box">
<input type="search" placeholder="Add torrent URL" v-model="torrentURL">
<input
type="search"
:disabled="loading"
:placeholder="placeholder"
v-model="torrentURL"
v-if="selectedView === 'transfers'"
>
<input
type="search"
:placeholder="placeholder"
:value="query"
ref="search"
v-else-if="selectedView === 'search'"
>
</label>
<span class="button-container">
<button type="submit" title="Loading" disabled v-if="loading">
<Loading />
</button>
<button type="submit" title="Add torrent URL" v-else-if="selectedView === 'transfers'">
<i class="fa fa-download" />
</button>
<button type="submit" title="Search" v-else-if="selectedView === 'search'">
<i class="fa fa-search" />
</button>
</span>
</form>
</div>
<div class="right side col-1" v-if="!withNav">
<button @click="$emit('toggle')" title="Toggle navigation">
<i class="fa fa-bars" />
</button>
</div>
</div>
</div>
</template>
<script>
import Loading from "@/components/Loading";
export default {
name: "Header",
emits: ['torrent-add'],
emits: ['torrent-add', 'search', 'toggle'],
components: {Loading},
props: {
query: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
withNav: {
type: Boolean,
default: false,
},
selectedView: {
type: String,
default: 'transfers',
},
},
data() {
return {
torrentURL: '',
}
},
computed: {
placeholder() {
if (this.selectedView === 'transfers') {
return 'Add torrent URL'
}
return 'Search torrents'
},
leftSideClasses() {
if (!this.withNav) {
return {
'col-12': true,
}
}
return {
'col-11': true,
}
},
},
methods: {
submit() {
const query = this.$refs?.search?.value?.trim()
if (this.selectedView === 'transfers' && this.torrentURL?.length) {
this.$emit('torrent-add', this.torrentURL)
} else if (this.selectedView === 'search' && query?.length) {
this.$emit('search', query)
}
}
},
}
</script>
@ -46,6 +138,9 @@ export default {
align-items: center;
&.right {
position: absolute;
font-size: 1.1em;
right: calc(#{$torrent-nav-width} / 4);
justify-content: right;
}
}
@ -54,7 +149,6 @@ export default {
background: none;
padding: 0 .25em;
border: 0;
margin-right: .25em;
&:hover {
color: $default-hover-fg-2;
@ -63,6 +157,7 @@ export default {
form {
width: 100%;
display: flex;
padding: 0;
border: 0;
border-radius: 0;
@ -70,8 +165,36 @@ export default {
background: initial;
}
.button-container {
width: 3em;
position: relative;
padding: 0;
}
:deep(.loading) {
width: 5em;
font-size: 1em;
left: -0.5em;
border-radius: 0 1em 1em 0;
}
[type=submit] {
width: 100%;
height: 100%;
background: $default-bg-4;
border: $default-border-2;
border-radius: 0 1em 1em 0;
margin-left: -.5em;
cursor: pointer;
&:hover {
background: $hover-bg-3;
}
}
.search-box {
width: 100%;
max-width: 600px;
margin-left: .5em;
input[type=search] {

View file

@ -0,0 +1,120 @@
<template>
<div class="info">
<div class="row">
<div class="label">Title</div>
<div class="value">{{ torrent.title }}</div>
</div>
<div class="row">
<div class="label">URL</div>
<div class="value">
<button title="Open" @click="openInNewTab(torrent.url)">
<i class="fas fa-up-right-from-square" />
</button>
<button title="Copy" @click="copyToClipboard(torrent.url)">
<i class="fas fa-clipboard" />
</button>
</div>
</div>
<div class="row">
<div class="label">Size</div>
<div class="value">{{ convertSize(torrent.size) }}</div>
</div>
<div class="row">
<div class="label">Seeders</div>
<div class="value">{{ torrent.seeds }}</div>
</div>
<div class="row">
<div class="label">Leechers</div>
<div class="value">{{ torrent.peers }}</div>
</div>
<div class="row">
<div class="label">Uploaded</div>
<div class="value">{{ formatDate(torrent.created_at, true) }}</div>
</div>
<div class="row" v-if="torrent.description">
<div class="label">Description</div>
<div class="value">{{ torrent.description }}</div>
</div>
<div class="row" v-if="torrent.year">
<div class="label">Year</div>
<div class="value">{{ torrent.year }}</div>
</div>
</div>
</template>
<script>
import Utils from "@/Utils";
export default {
mixins: [Utils],
props: {
torrent: {
type: Object,
default: () => ({}),
},
},
methods: {
openInNewTab(url) {
window.open(url, "_blank");
},
},
}
</script>
<style lang="scss" scoped>
.info {
min-width: 50vw;
max-width: 800px;
.row {
display: flex;
align-items: center;
margin-bottom: 5px;
border-bottom: $default-border;
@include until($desktop) {
flex-direction: column;
}
@include from($desktop) {
flex-direction: row;
}
}
.label {
width: 10em;
font-weight: bold;
margin-right: 5px;
}
.value {
width: calc(100% - 10em);
flex: 1;
text-align: right;
button {
background: none;
border: none;
cursor: pointer;
i {
font-size: 1.2em;
}
&:hover {
color: $default-hover-fg;
}
}
}
}
</style>

View file

@ -0,0 +1,109 @@
<template>
<nav>
<button class="menu-button" @click="$emit('toggle')">
<i class="fa fa-bars" />
</button>
<li v-for="(view, name) in views" :key="name" :title="view.displayName"
:class="{selected: name === selectedView}" @click="$emit('input', name)">
<i :class="view.iconClass" />
</li>
</nav>
</template>
<script>
export default {
emits: ['input', 'toggle'],
props: {
selectedView: {
type: String,
},
collapsed: {
type: Boolean,
default: false,
},
views: {
type: Object,
default: () => {
return {
search: {
displayName: 'Search',
iconClass: 'fa fa-search',
},
transfers: {
displayName: 'Transfers',
iconClass: 'fa fa-download',
},
}
}
},
},
}
</script>
<style lang="scss" scoped>
@import "vars";
nav {
width: $torrent-nav-width;
height: calc(100% + #{$torrent-header-height});
margin-top: -$torrent-header-height;
background: $nav-collapsed-bg;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
box-shadow: 2.5px 0 4.5px 2px $nav-collapsed-fg;
margin-left: 2.5px;
overflow: hidden;
.menu-button {
position: absolute;
top: 0.75em;
left: 0;
width: 100%;
display: inline-flex;
justify-content: center;
padding: 0;
margin: 0;
font-size: 1.2em;
border: 0;
background: none;
cursor: pointer;
z-index: 1;
&:hover {
color: $default-hover-fg;
}
}
li {
display: flex;
align-items: center;
font-size: 1.2em;
cursor: pointer;
list-style: none;
padding: .6em;
opacity: 0.7;
&.selected,
&:hover {
border-radius: 1.2em;
margin: 0 0.2em;
}
&:hover {
background: $nav-entry-collapsed-hover-bg;
}
&.selected {
background: $nav-entry-collapsed-selected-bg;
}
}
}
</style>

View file

@ -1,24 +1,78 @@
<template>
<div class="torrent-container">
<div class="header-container">
<Header @torrent-add="download($event)" />
<Modal
title="Torrent info"
:visible="infoItem !== null"
@close="infoIndex = null"
v-if="infoItem"
>
<Info :torrent="infoItem" />
</Modal>
<div class="header-container" :class="{'with-nav': !navCollapsed}">
<Header
:with-nav="!navCollapsed"
:selected-view="selectedView"
:loading="loading"
@search="search($event)"
@torrent-add="download($event)"
@toggle="navCollapsed = !navCollapsed"
/>
</div>
<div class="view-container">
<TorrentView :plugin-name="pluginName" />
</div>
<main>
<div class="view-container" :class="{'with-nav': !navCollapsed}">
<Transfers
:transfers="transfers"
@pause="pause($event)"
@resume="resume($event)"
@remove="remove($event)"
v-if="selectedView === 'transfers'"
/>
<Results
:results="results"
@download="download($event)"
@info="infoIndex = $event"
@next-page="search(query, page + 1)"
v-else-if="selectedView === 'search'"
/>
</div>
<div class="nav-container">
<Nav
:selected-view="selectedView"
@toggle="navCollapsed = !navCollapsed"
@input="selectedView = $event"
v-if="!navCollapsed"
/>
</div>
</main>
</div>
</template>
<script>
import Info from "./Info";
import Header from "@/components/panels/Torrent/Header";
import TorrentView from "@/components/panels/Torrent/View";
import Modal from "@/components/Modal";
import Nav from "@/components/panels/Torrent/Nav";
import Results from "@/components/panels/Torrent/Results";
import Transfers from "@/components/panels/Torrent/Transfers";
import Utils from "@/Utils";
export default {
name: "Panel",
components: {TorrentView, Header},
mixins: [Utils],
components: {
Info,
Header,
Modal,
Nav,
Results,
Transfers,
},
props: {
pluginName: {
type: String,
@ -26,25 +80,250 @@ export default {
},
},
data() {
return {
loading: false,
transfers: {},
results: [],
selectedView: "transfers",
navCollapsed: false,
query: "",
page: 1,
infoIndex: null,
};
},
computed: {
infoItem() {
if (this.infoIndex === null) {
return null
}
return this.results[this.infoIndex]
},
},
methods: {
torrentId(torrent) {
if (torrent?.hash && torrent.hash.length)
return torrent.hash
return torrent.url
},
onTorrentUpdate(torrent) {
this.transfers[this.torrentId(torrent)] = torrent
},
onTorrentQueued(torrent) {
this.onTorrentUpdate(torrent)
this.notify({
text: 'Torrent queued for download',
image: {
icon: 'hourglass-start',
}
})
},
onTorrentStart(torrent) {
this.onTorrentUpdate(torrent)
this.notify({
html: `Torrent download started: <b>${torrent.name}</b>`,
image: {
icon: 'play',
}
})
},
onTorrentResume(torrent) {
this.onTorrentUpdate(torrent)
this.notify({
html: `Torrent download resumed: <b>${torrent.name}</b>`,
image: {
icon: 'play',
}
})
},
onTorrentPause(torrent) {
this.onTorrentUpdate(torrent)
this.notify({
html: `Torrent download paused: <b>${torrent.name}</b>`,
image: {
icon: 'pause',
}
})
},
onTorrentCompleted(torrent) {
this.onTorrentUpdate(torrent)
this.transfers[this.torrentId(torrent)].finish_date = new Date().toISOString()
this.transfers[this.torrentId(torrent)].progress = 100
this.notify({
html: `Torrent download completed: <b>${torrent.name}</b>`,
image: {
icon: 'check',
}
})
},
onTorrentRemove(torrent) {
const torrentId = this.torrentId(torrent)
if (torrentId in this.transfers)
delete this.transfers[torrentId]
},
async search(query, page=1) {
this.loading = true
this.query = query
let results = []
try {
results = await this.request(
`${this.pluginName}.search`,
{query: query, page: page}
)
} finally {
this.loading = false
}
this.results = page === 1 ? results : this.results.concat(results)
if (results.length > 0) {
this.page = page
}
},
async download(torrent) {
await this.request(`${this.pluginName}.download`, {torrent: torrent})
},
async pause(torrent) {
await this.request(`${this.pluginName}.pause`, {torrent: torrent.url})
await this.refresh()
},
async resume(torrent) {
await this.request(`${this.pluginName}.resume`, {torrent: torrent.url})
await this.refresh()
},
async remove(torrent) {
await this.request(`${this.pluginName}.remove`, {torrent: torrent.url})
await this.refresh()
},
async refresh() {
this.loading = true
try {
this.transfers = Object.values(await this.request(`${this.pluginName}.status`) || {})
.reduce((obj, torrent) => {
obj[this.torrentId(torrent)] = torrent
return obj
}, {})
} finally {
this.loading = false
}
},
},
mounted() {
this.refresh()
this.selectedView = this.transfers.length ? 'transfers' : 'search'
this.subscribe(
this.onTorrentUpdate,
'on-torrent-update',
'platypush.message.event.torrent.TorrentDownloadStartEvent',
'platypush.message.event.torrent.TorrentDownloadProgressEvent',
'platypush.message.event.torrent.TorrentSeedingStartEvent',
'platypush.message.event.torrent.TorrentStateChangeEvent',
)
this.subscribe(
this.onTorrentQueued,
'on-torrent-queued',
'platypush.message.event.torrent.TorrentQueuedEvent',
)
this.subscribe(
this.onTorrentStart,
'on-torrent-queued',
'platypush.message.event.torrent.TorrentDownloadedMetadataEvent',
)
this.subscribe(
this.onTorrentResume,
'on-torrent-resume',
'platypush.message.event.torrent.TorrentResumedEvent',
)
this.subscribe(
this.onTorrentPause,
'on-torrent-pause',
'platypush.message.event.torrent.TorrentPausedEvent',
)
this.subscribe(
this.onTorrentStop,
'on-torrent-stop',
'platypush.message.event.torrent.TorrentDownloadStopEvent',
)
this.subscribe(
this.onTorrentCompleted,
'on-torrent-completed',
'platypush.message.event.torrent.TorrentDownloadCompletedEvent'
)
this.subscribe(
this.onTorrentRemove,
'on-torrent-remove',
'platypush.message.event.torrent.TorrentRemovedEvent'
)
const searchBox = document.querySelector('.search-box input[type="search"]')
if (searchBox) {
this.$nextTick(() => searchBox.focus())
}
},
destroy() {
this.unsubscribe('on-torrent-update')
this.unsubscribe('on-torrent-remove')
},
}
</script>
<style lang="scss" scoped>
@import "vars";
.header-container {
&.with-nav {
width: calc(100% - #{$torrent-nav-width});
}
}
.torrent-container {
width: 100%;
height: calc(100% - #{$torrent-header-height});
height: 100%;
position: relative;
main {
width: 100%;
height: calc(100% - #{$torrent-header-height});
display: flex;
}
.view-container {
width: 100%;
height: 100%;
overflow: auto;
padding-top: .2em;
&.with-nav {
width: calc(100% - #{$torrent-nav-width});
}
}
}
</style>

View file

@ -0,0 +1,188 @@
<template>
<div class="results-container">
<div class="no-content" v-if="!results?.length">No results</div>
<div class="results" ref="body" @scroll="onScroll" v-else>
<div class="result" v-for="(result, i) in results" :key="i">
<div class="info">
<div class="title">{{ result.title }}</div>
<div class="additional-info">
<span class="info-pill size">
<span class="label">
<i class="fa fa-hdd" />
</span>
<span class="separator" />
<span class="value">{{ convertSize(result.size) }}</span>
</span>
<span class="separator"> &vert; </span>
<span class="info-pill seeds">
<span class="label">
<i class="fa fa-users" />
</span>
<span class="separator" />
<span class="value">{{ result.seeds }}</span>
</span>
<span class="separator"> &vert; </span>
<span class="info-pill created-at">
<span class="label">
<i class="fa fa-calendar" />
</span>
<span class="separator" />
<span class="value">{{ formatDate(result.created_at, true) }}</span>
</span>
<span class="separator"> &vert; </span>
</div>
</div>
<div class="actions">
<button title="Torrent info" @click="$emit('info', i)">
<i class="fa fa-info-circle" />
</button>
<button title="Download" @click="$emit('download', result.url)">
<i class="fa fa-download" />
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import Utils from "@/Utils";
export default {
emits: ['download', 'info', 'next-page'],
mixins: [Utils],
props: {
results: {
type: Array,
default: () => [],
},
page: {
type: Number,
default: 1,
},
},
data() {
return {
scrollTimeout: null,
}
},
methods: {
onScroll() {
const offset = this.$refs.body.scrollTop
const bodyHeight = parseFloat(getComputedStyle(this.$refs.body).height)
const scrollHeight = this.$refs.body.scrollHeight
if (offset >= (scrollHeight - bodyHeight - 5)) {
if (this.scrollTimeout || !this.results.length)
return
this.scrollTimeout = setTimeout(() => {
this.scrollTimeout = null
}, 250)
this.$emit('next-page', this.page + 1)
}
},
},
}
</script>
<style lang="scss" scoped>
.results-container {
width: 100%;
height: 100%;
background: $background-color;
display: flex;
flex-wrap: wrap;
.no-content {
width: 100%;
height: 100%;
text-align: center;
}
.results {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.result {
width: 100%;
display: flex;
border-bottom: $default-border;
gap: 1em;
padding: .5em 0;
cursor: pointer;
&:hover {
background: $hover-bg;
}
.info {
width: calc(100% - 5em);
padding: .5em;
}
.additional-info {
font-size: .8em;
opacity: .7;
display: flex;
.separator {
font-size: 1.5em;
margin-right: .5em;
}
.info-pill {
display: flex;
align-items: center;
margin-right: .5em;
.label {
font-weight: bold;
}
.separator {
margin: -.2em .25em 0 .25em;
font-size: 2em;
}
}
}
.actions {
display: flex;
align-items: center;
justify-content: center;
width: 5em;
button {
background: none;
opacity: .8;
border: none;
padding: .25em;
cursor: pointer;
&:hover {
color: $hover-fg;
}
&:first-child {
margin-right: 1em;
}
}
}
}
}
</style>

View file

@ -9,8 +9,8 @@
<div class="col-8 left side">
<i class="icon fa" :class="{
'fa-check': torrent.finish_date != null,
'fa-play': !torrent.finish_date && torrent.state === 'downloading',
'fa-pause': !torrent.finish_date && torrent.state === 'paused',
'fa-play': !torrent.finish_date && torrent.state === 'downloading' && !torrent.paused,
'fa-pause': !torrent.finish_date && torrent.state === 'downloading' && torrent.paused,
'fa-stop': !torrent.finish_date && torrent.state === 'stopped',
}" />
<div class="title" v-text="torrent.name || torrent.hash || torrent.url" />
@ -22,11 +22,11 @@
<div class="col-2 right side">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" @click="selectedItem = i">
<DropdownItem icon-class="fa fa-pause" text="Pause transfer" @click="pause(torrentId(torrent))"
v-if="torrent.state === 'downloading'" />
<DropdownItem icon-class="fa fa-play" text="Resume transfer" @click="resume(torrentId(torrent))"
v-if="torrent.state === 'paused'" />
<DropdownItem icon-class="fa fa-trash" text="Remove transfer" @click="remove(torrentId(torrent))" />
<DropdownItem icon-class="fa fa-pause" text="Pause transfer" @click="$emit('pause', torrent)"
v-if="torrent.state === 'downloading' && !torrent.paused" />
<DropdownItem icon-class="fa fa-play" text="Resume transfer" @click="$emit('resume', torrent)"
v-if="torrent.state === 'downloading' && torrent.paused" />
<DropdownItem icon-class="fa fa-trash" text="Remove transfer" @click="$emit('remove', torrent)" />
<DropdownItem icon-class="fa fa-folder" text="View files" @click="$refs.torrentFiles.isVisible = true" />
<DropdownItem icon-class="fa fa-info" text="Torrent info" @click="$refs.torrentInfo.isVisible = true" />
</Dropdown>
@ -96,6 +96,15 @@
<div class="attr">Save path</div>
<div class="value" v-text="transfers[selectedItem].save_path" />
</div>
<div class="row" v-if="transfers[selectedItem].files">
<div class="attr">Files</div>
<div class="value">
<div class="file" v-for="(file, i) in transfers[selectedItem].files" :key="i">
<a :href="`/file?path=${encodeURIComponent(file)}`" target="_blank" v-text="file" />
</div>
</div>
</div>
</div>
</Modal>
@ -126,26 +135,31 @@ import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
export default {
name: "View",
emits: ['play', 'play-with-captions'],
emits: [
'pause',
'play',
'play-with-captions',
'refresh',
'remove',
'resume',
],
components: {Dropdown, DropdownItem, Loading, Modal},
mixins: [Utils, MediaUtils],
props: {
pluginName: {
type: String,
required: true,
},
isMedia: {
type: Boolean,
default: false,
},
transfers: {
type: Object,
default: () => ({}),
},
},
data() {
return {
loading: false,
transfers: {},
selectedItem: null,
}
},
@ -158,79 +172,6 @@ export default {
return this.transfers[this.selectedItem].files.map((file) => file.split('/').pop())
},
},
methods: {
torrentId(torrent) {
if (torrent?.hash && torrent.hash.length)
return torrent.hash
return torrent.url
},
async refresh() {
this.loading = true
try {
this.transfers = Object.values(await this.request(`${this.pluginName}.status`) || {})
.reduce((obj, torrent) => {
obj[this.torrentId(torrent)] = torrent
return obj
}, {})
} finally {
this.loading = false
}
},
async pause(torrent) {
await this.request(`${this.pluginName}.pause`, {torrent: torrent})
await this.refresh()
},
async resume(torrent) {
await this.request(`${this.pluginName}.resume`, {torrent: torrent})
await this.refresh()
},
async remove(torrent) {
await this.request(`${this.pluginName}.remove`, {torrent: torrent})
await this.refresh()
},
onTorrentUpdate(torrent) {
this.transfers[this.torrentId(torrent)] = torrent
},
onTorrentRemove(torrent) {
const torrentId = this.torrentId(torrent)
if (torrentId in this.transfers)
delete this.transfers[torrentId]
},
},
mounted() {
this.refresh()
this.subscribe(
this.onTorrentUpdate,'on-torrent-update',
'platypush.message.event.torrent.TorrentQueuedEvent',
'platypush.message.event.torrent.TorrentDownloadedMetadataEvent',
'platypush.message.event.torrent.TorrentDownloadStartEvent',
'platypush.message.event.torrent.TorrentDownloadProgressEvent',
'platypush.message.event.torrent.TorrentResumedEvent',
'platypush.message.event.torrent.TorrentPausedEvent',
'platypush.message.event.torrent.TorrentSeedingStartEvent',
'platypush.message.event.torrent.TorrentStateChangeEvent',
'platypush.message.event.torrent.TorrentDownloadStopEvent',
'platypush.message.event.torrent.TorrentDownloadCompletedEvent')
this.subscribe(this.onTorrentRemove,'on-torrent-remove',
'platypush.message.event.torrent.TorrentRemovedEvent')
},
destroy() {
this.unsubscribe('on-torrent-update')
this.unsubscribe('on-torrent-remove')
},
}
</script>

View file

@ -1 +1,2 @@
$torrent-header-height: 3.3em;
$torrent-nav-width: 2.8em;

View file

@ -38,6 +38,7 @@ module.exports = {
'^/ws/requests': wsProxy,
'^/ws/shell': wsProxy,
'^/execute': httpProxy,
'^/file': httpProxy,
'^/auth': httpProxy,
'^/camera/': httpProxy,
'^/sound/': httpProxy,

View file

@ -6,10 +6,19 @@ class TorrentMediaSearcher(MediaSearcher):
def search(self, query, **kwargs):
self.logger.info('Searching torrents for "{}"'.format(query))
torrents = get_plugin(self.media_plugin.torrent_plugin if self.media_plugin else 'torrent')
torrents = get_plugin(
self.media_plugin.torrent_plugin if self.media_plugin else 'torrent'
)
if not torrents:
raise RuntimeError('Torrent plugin not available/configured')
return torrents.search(query, ).output
return [
torrent
for torrent in torrents.search(
query,
).output
if torrent.get('is_media')
]
# vim:sw=4:ts=4:et: