perf: Rework search engine (#819)

This commit is contained in:
Rémi Marseault 2023-05-31 11:33:19 +02:00 committed by GitHub
parent 17aff9e034
commit 834827fed4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 551 additions and 422 deletions

View file

@ -73,8 +73,8 @@ export default {
this.newPath = this.torrents[0].savePath
},
methods: {
setLocation() {
qbit.setTorrentLocation(this.hashes, this.newPath)
async setLocation() {
await qbit.setTorrentLocation(this.hashes, this.newPath)
this.close()
},
close() {

View file

@ -54,8 +54,8 @@ export default {
close() {
this.dialog = false
},
deleteTorrent() {
qbit.deleteTorrents(this.selected_torrents, this.settings.deleteWithFiles)
async deleteTorrent() {
await qbit.deleteTorrents(this.selected_torrents, this.settings.deleteWithFiles)
this.close()
}
}

View file

@ -67,16 +67,16 @@ export default defineComponent({
document.removeEventListener('keydown', this.handleKeyboardShortcut)
},
methods: {
create() {
qbit.createFeed(this.feed)
async create() {
await qbit.createFeed(this.feed)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_FEEDS')
this.dialog = false
},
edit() {
qbit.editFeed(this.initialFeed.name, this.feed.name)
async edit() {
await qbit.editFeed(this.initialFeed.name, this.feed.name)
this.$toast.success(this.$t('toast.feedSaved'))
this.cancel()
},

View file

@ -1,62 +0,0 @@
<template>
<div>
<v-btn @click="opened = true">
<v-icon>{{ mdiCog }}</v-icon> {{ $t('modals.pluginManager.title') | titleCase }}
</v-btn>
<v-dialog v-model="opened" width="50%">
<v-card class="pa-0">
<v-card-title class="justify-center pa-1">
<v-toolbar flat dense class="transparent">
<v-toolbar-title>
<v-icon>{{ mdiToyBrick }}</v-icon> Plugin manager
</v-toolbar-title>
<v-spacer />
<v-btn fab small class="transparent elevation-0" @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-switch v-for="(plugin, key) in searchPlugins" :key="key" v-model="plugin.enabled" :label="plugin.fullName" @change="togglePlugin(plugin)" />
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import { mapState } from 'vuex'
import qbit from '@/services/qbit'
import { mdiCog, mdiToyBrick, mdiClose } from '@mdi/js'
export default {
name: 'PluginsManager',
data: () => ({
opened: false,
mdiCog,
mdiToyBrick,
mdiClose
}),
computed: {
...mapState(['searchPlugins'])
},
watch: {
opened() {
this.$store.commit('FETCH_SEARCH_PLUGINS')
}
},
mounted() {
if (!this.searchPlugins.length) {
qbit.updateSearchPlugins()
}
},
methods: {
togglePlugin(plugin) {
qbit.enableSearchPlugin([plugin.name], plugin.enabled)
},
close() {
this.opened = false
}
}
}
</script>

View file

@ -1,195 +0,0 @@
<template>
<v-dialog v-model="dialog" scrollable :width="dialogWidth" :fullscreen="phoneLayout" :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card :style="{ height: phoneLayout ? '100vh' : '' }">
<v-card-text class="pa-0">
<v-form ref="form" v-model="searchForm.valid">
<v-flex row class="my-1 py-1 px-2 mx-auto">
<v-col class="pa-0" cols="8">
<v-text-field
v-model="searchForm.pattern"
:prepend-inner-icon="mdiMagnify"
label="Search"
:rules="[v => !!v || 'Search term is required']"
clearable
style="width: 95%"
autofocus
@keydown.enter.prevent="$refs.searchButton.click"
/>
</v-col>
<v-col class="pa-0 mt-2" cols="3">
<v-btn ref="searchButton" class="mt-2 mx-0" :disabled="!searchForm.valid" :color="loading ? 'warning' : 'primary'" @click="loading ? stopSearch() : startSearch()">
{{ loading ? $t('modals.search.btnStopSearch') : $t('modals.search.btnStartSearch') }}
</v-btn>
</v-col>
</v-flex>
</v-form>
<v-data-table
id="searchTable"
:headers="grid.headers"
:items="search.results"
:items-per-page="10"
:loading="loading"
:style="{ maxHeight: '60vh' }"
:search="filter"
:custom-filter="customFilter"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
>
<template #top>
<v-text-field ref="filterRef" v-model="filter" :label="$t('filter')" class="mx-4" />
</template>
<template #[`item.fileName`]="{ item }">
<a :href="item.descrLink" target="_blank" v-text="item.fileName" />
</template>
<template #[`item.fileSize`]="{ item }">
{{ item.fileSize | formatSize }}
</template>
<template #[`item.actions`]="{ item }">
<v-icon @click="downloadTorrent(item)">
{{ mdiDownload }}
</v-icon>
</template>
</v-data-table>
</v-card-text>
<v-card-actions>
<PluginManager />
</v-card-actions>
<v-fab-transition v-if="phoneLayout">
<v-btn color="red" dark absolute bottom right @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-fab-transition>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Modal, FullScreenModal, General } from '@/mixins'
import PluginManager from './PluginManager.vue'
import { mdiClose, mdiMagnify, mdiDownload } from '@mdi/js'
export default {
name: 'SearchModal',
components: { PluginManager },
mixins: [Modal, FullScreenModal, General],
data() {
return {
search: {
id: null,
status: null,
interval: null,
results: []
},
loading: false,
grid: {
headers: [
{ text: this.$t('modals.search.columnTitle.name'), value: 'fileName' },
{ text: this.$t('modals.search.columnTitle.size'), value: 'fileSize' },
{ text: this.$t('modals.search.columnTitle.seeds'), value: 'nbSeeders' },
{ text: this.$t('modals.search.columnTitle.peers'), value: 'nbLeechers' },
{
text: this.$t('modals.search.columnTitle.search_engine'),
value: 'siteUrl'
},
{
text: this.$t('modals.search.columnTitle.action'),
value: 'actions',
sortable: false
}
]
},
searchForm: {
valid: false,
pattern: ''
},
filter: '',
mdiClose,
mdiMagnify,
mdiDownload,
sortBy: 'nbSeeders',
sortDesc: true
}
},
computed: {
...mapGetters(['getSearchPlugins']),
dialogWidth() {
return this.phoneLayout ? '100%' : '70%'
},
enabledSearchPlugins() {
return this.getSearchPlugins().filter(p => p.enabled)
}
},
created() {
this.$store.commit('FETCH_SEARCH_PLUGINS')
},
methods: {
async startSearch() {
if (this.searchForm.pattern.length && !this.search.interval) {
this.loading = true
this.search.status = 'Running'
this.search.results = []
this.$refs.filterRef.reset()
const data = await qbit.startSearch(
this.searchForm.pattern,
this.enabledSearchPlugins.map(p => p.name)
)
this.search.id = data.id
await this.getStatus()
this.search.interval = setInterval(async () => {
const status = await this.getStatus()
if (status === 'Stopped') {
clearInterval(this.search.interval)
this.search.interval = null
}
await this.getResults()
}, 500)
}
},
async getStatus() {
if (this.search.id) {
const data = await qbit.getSearchStatus(this.search.id)
return (this.search.status = data[0].status)
}
},
async getResults() {
const data = await qbit.getSearchResults(this.search.id)
this.search.results = data.results
},
downloadTorrent(item) {
this.createModal('addModal', { initialMagnet: item.fileUrl })
},
stopSearch() {
qbit.stopSearch(this.search.id)
this.loading = false
},
close() {
this.dialog = false
},
customFilter(value, search, item) {
const searchArr = search.trim().toLowerCase().split(' ')
return value != null && search != null && typeof value === 'string' && searchArr.every(i => value.toString().toLowerCase().indexOf(i) !== -1)
}
}
}
</script>
<style lang="scss">
#searchTable {
.v-data-footer {
justify-content: center;
}
.v-data-footer__pagination {
margin: 0 8px;
}
.v-select__slot {
width: 4em;
min-width: 100%;
}
}
</style>

View file

@ -0,0 +1,173 @@
<template>
<v-dialog v-model="dialog" scrollable :width="dialogWidth" :fullscreen="phoneLayout">
<v-card>
<v-card-title class="pa-0">
<v-toolbar-title class="ma-4 primarytext--text">
<h3>{{ $t('modals.searchPluginManager.title') }}</h3>
</v-toolbar-title>
<v-spacer />
<v-dialog v-model="installDialog" max-width="500px">
<template v-slot:activator="{ on, attrs }">
<v-btn class="primary white--text elevation-0 px-4 ma-4" v-on="on">Install</v-btn>
</template>
<v-card>
<v-card-title>
<h5>{{ $t('modals.searchPluginManager.install.title') }}</h5>
</v-card-title>
<v-card-text>
<v-text-field v-model="installInput" :label="$t('modals.searchPluginManager.install.label')" required />
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="accent darken-1" text @click="closeInstallDialog">{{ $t('cancel') }}</v-btn>
<v-btn color="accent darken-1" text @click="installNewPlugin">{{ $t('ok') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-title>
<v-card-text>
<v-data-table
style="width: 100%"
v-model="enabledPlugins"
item-key="name"
:headers="headers"
:items="searchPlugins"
disable-pagination
hide-default-footer
show-select
sort-by="name"
:loading="loading"
@item-selected="onTogglePlugin"
@toggle-select-all="onToggleAllPlugins"
>
<template v-slot:item.actions="{ item }">
<v-icon color="red" small @click="uninstallPlugin(item)">
{{ mdiDelete }}
</v-icon>
</template>
</v-data-table>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn class="white--text elevation-0 px-4" @click="checkForPluginUpdates">
{{ $t('modals.searchPluginManager.checkForUpdates') }}
</v-btn>
<v-btn class="accent white--text elevation-0 px-4" @click="close">
{{ $t('close') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { FullScreenModal, Modal } from '@/mixins'
import { mapState } from 'vuex'
import { SearchPlugin } from '@/types/qbit/models'
import qbit from '@/services/qbit'
import { mdiDelete } from '@mdi/js'
export default defineComponent({
name: 'SearchPluginManager',
mixins: [Modal, FullScreenModal],
data() {
return {
headers: [
{ text: 'Name', value: 'fullName' },
{ text: 'Version', value: 'version' },
{ text: 'URL', value: 'url' },
{ text: 'Actions', value: 'actions' }
],
enabledPlugins: [],
loading: false,
installDialog: false,
installInput: '',
mdiDelete
}
},
computed: {
...mapState(['searchPlugins'])
},
mounted() {
document.addEventListener('keydown', this.handleKeyboardShortcut)
this.loading = true
this.updatePluginList()
this.loading = false
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyboardShortcut)
},
methods: {
async updatePluginList() {
await this.$store.dispatch('FETCH_SEARCH_PLUGINS')
this.enabledPlugins = this.searchPlugins.filter((plugin: SearchPlugin) => plugin.enabled)
},
async onTogglePlugin(payload: { item: SearchPlugin; value: boolean }) {
this.loading = true
await qbit.enableSearchPlugin([payload.item.name], payload.value)
this.loading = false
},
async onToggleAllPlugins(payload: { items: SearchPlugin[]; value: boolean }) {
this.loading = true
await qbit.enableSearchPlugin(
payload.items.map(plugin => plugin.name),
payload.value
)
this.loading = false
},
async installNewPlugin() {
this.loading = true
this.closeInstallDialog()
await qbit.installSearchPlugin([this.installInput])
this.installInput = ''
await this.updatePluginList()
this.loading = false
},
async checkForPluginUpdates() {
this.loading = true
await qbit.updateSearchPlugins()
await this.updatePluginList()
this.loading = false
},
async uninstallPlugin(plugin: SearchPlugin) {
this.loading = true
await qbit.uninstallSearchPlugin([plugin.name])
await this.updatePluginList()
this.loading = false
},
close() {
this.dialog = false
},
closeInstallDialog() {
this.installDialog = false
},
handleKeyboardShortcut(e: KeyboardEvent) {
if (e.key === 'Escape') {
this.close()
}
}
}
})
</script>

View file

@ -46,8 +46,8 @@ export default {
}
},
methods: {
save() {
qbit.setShareLimit([this.hash], this.$refs.ratio.export(), this.$refs.time.export())
async save() {
await qbit.setShareLimit([this.hash], this.$refs.ratio.export(), this.$refs.time.export())
this.close()
},
close() {

View file

@ -84,20 +84,20 @@ export default {
}
},
methods: {
setLimit() {
async setLimit() {
switch (this.mode) {
case 'download':
if (this.isGlobal()) {
qbit.setGlobalDownloadLimit(this.exportLimit())
await qbit.setGlobalDownloadLimit(this.exportLimit())
} else {
qbit.setDownloadLimit([this.hash], this.exportLimit())
await qbit.setDownloadLimit([this.hash], this.exportLimit())
}
break
case 'upload':
if (this.isGlobal()) {
qbit.setGlobalUploadLimit(this.exportLimit())
await qbit.setGlobalUploadLimit(this.exportLimit())
} else {
qbit.setUploadLimit([this.hash], this.exportLimit())
await qbit.setUploadLimit([this.hash], this.exportLimit())
}
break
default:
@ -107,7 +107,7 @@ export default {
this.close()
},
isGlobal() {
return this.torrent ? false : true
return !this.torrent
},
formatLimit(limit) {
return limit > 0 ? limit / 1024 : '∞'

View file

@ -69,16 +69,16 @@ export default {
}
},
methods: {
create() {
qbit.createCategory(this.category)
async create() {
await qbit.createCategory(this.category)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_CATEGORIES')
this.dialog = false
},
edit() {
qbit.editCategory(this.category)
async edit() {
await qbit.editCategory(this.category)
Vue.$toast.success(this.$t('toast.categorySaved'))
this.cancel()
}

View file

@ -41,8 +41,8 @@ export default {
this.$store.commit('FETCH_TAGS')
},
methods: {
create() {
qbit.createTag([this.tagname])
async create() {
await qbit.createTag([this.tagname])
this.cancel()
},
cancel() {

View file

@ -101,8 +101,8 @@ export default {
this.$store.commit('LOGOUT')
this.$router.push({ name: 'login' })
},
toggleSpeed() {
qbit.toggleSpeedLimitsMode()
async toggleSpeed() {
await qbit.toggleSpeedLimitsMode()
},
toggleTheme() {
this.webuiSettings.darkTheme = !this.webuiSettings.darkTheme

View file

@ -42,7 +42,7 @@
</v-tooltip>
<v-tooltip bottom open-delay="400">
<template #activator="{ on }">
<v-btn :text="!mobile" small fab color="grey--text" class="mr-0 ml-0" :aria-label="$t('navbar.topActions.searchNew')" v-on="on" @click="addModal('SearchModal')">
<v-btn :text="!mobile" small fab color="grey--text" class="mr-0 ml-0" :aria-label="$t('navbar.topActions.searchNew')" v-on="on" @click="goToSearch">
<v-icon color="grey">
{{ mdiSearchWeb }}
</v-icon>
@ -103,11 +103,11 @@ export default {
...mapState(['selected_torrents'])
},
methods: {
pauseTorrents() {
qbit.pauseTorrents(this.selected_torrents)
async pauseTorrents() {
await qbit.pauseTorrents(this.selected_torrents)
},
resumeTorrents() {
qbit.resumeTorrents(this.selected_torrents)
async resumeTorrents() {
await qbit.resumeTorrents(this.selected_torrents)
},
removeTorrents() {
if (!this.selected_torrents.length) return
@ -117,6 +117,9 @@ export default {
addModal(name) {
this.createModal(name)
},
goToSearch() {
if (this.$route.name !== 'search') this.$router.push({ name: 'search' })
},
goToRss() {
if (this.$route.name !== 'rss') this.$router.push({ name: 'rss' })
},

View file

@ -76,8 +76,8 @@ export default defineComponent({
editFeed(item: Feed) {
this.createModal('FeedForm', { initialFeed: { url: item.url, name: item.name } })
},
deleteFeed(item: Feed) {
qbit.deleteFeed(item.name)
async deleteFeed(item: Feed) {
await qbit.deleteFeed(item.name)
this.$store.commit('FETCH_FEEDS')
},
createFeed() {

View file

@ -1,7 +1,7 @@
<template>
<v-card flat>
<v-row dense class="ma-0 pa-0">
<v-col cols="12" sm="6" lg="3" v-for="(item, index) in availableRules" :key="item.uid">
<v-col cols="12" sm="6" lg="3" v-for="item in availableRules" :key="item.uid">
<v-list-item>
<v-list-item-content>
<v-list-item-title v-text="item.name" />
@ -62,8 +62,8 @@ export default {
activeMethod() {
this.$store.commit('FETCH_RULES')
},
deleteRule(item) {
qbit.deleteRule(item.name)
async deleteRule(item) {
await qbit.deleteRule(item.name)
this.$store.commit('FETCH_RULES')
},
createRule() {

View file

@ -95,12 +95,12 @@ export default {
createCategory() {
this.createModal('CreateCategoryDialog')
},
deleteCategory(category) {
qbit.deleteCategory([category.name])
async deleteCategory(category) {
await qbit.deleteCategory([category.name])
this.$store.commit('FETCH_CATEGORIES')
},
deleteTag(item) {
qbit.deleteTag([item])
async deleteTag(item) {
await qbit.deleteTag([item])
this.$store.commit('FETCH_TAGS')
},
editCategory(cat) {

View file

@ -387,11 +387,11 @@ export default {
detectRightside() {
this.isRightside = document.documentElement.clientWidth < this.x + 380
},
resume() {
qbit.resumeTorrents(this.hashes)
async resume() {
await qbit.resumeTorrents(this.hashes)
},
pause() {
qbit.pauseTorrents(this.hashes)
async pause() {
await qbit.pauseTorrents(this.hashes)
},
location() {
this.createModal('ChangeLocationModal', { hashes: this.multiple ? this.selected_torrents : [this.torrent.hash] })
@ -399,22 +399,22 @@ export default {
rename() {
this.createModal('RenameModal', { hash: this.torrent.hash })
},
reannounce() {
qbit.reannounceTorrents(this.hashes)
async reannounce() {
await qbit.reannounceTorrents(this.hashes)
},
removeTorrent() {
this.$store.state.selected_torrents = this.hashes
return this.createModal('ConfirmDeleteModal')
},
recheck() {
qbit.recheckTorrents(this.hashes)
async recheck() {
await qbit.recheckTorrents(this.hashes)
},
showInfo() {
this.$router.push({ name: 'torrentDetail', params: { hash: this.torrent.hash } })
},
setPriority(priority) {
qbit.setTorrentPriority(this.hashes, priority)
async setPriority(priority) {
await qbit.setTorrentPriority(this.hashes, priority)
},
setLimit(mode) {
this.createModal('SpeedLimitModal', { hash: this.torrent.hash, mode })
@ -422,30 +422,30 @@ export default {
setShareLimit() {
this.createModal('ShareLimitModal', { hash: this.torrent.hash })
},
forceResume() {
qbit.forceStartTorrents(this.hashes)
async forceResume() {
await qbit.forceStartTorrents(this.hashes)
},
setCategory(cat) {
qbit.setCategory(this.hashes, cat)
async setCategory(cat) {
await qbit.setCategory(this.hashes, cat)
},
setTag(tag) {
if (this.torrent.tags && this.torrent.tags.includes(tag)) this.removeTag(tag)
else this.addTag(tag)
async setTag(tag) {
if (this.torrent.tags && this.torrent.tags.includes(tag)) await this.removeTag(tag)
else await this.addTag(tag)
},
addTag(tag) {
qbit.addTorrentTag(this.hashes, [tag])
async addTag(tag) {
await qbit.addTorrentTag(this.hashes, [tag])
},
removeTag(tag) {
qbit.removeTorrentTag(this.hashes, [tag])
async removeTag(tag) {
await qbit.removeTorrentTag(this.hashes, [tag])
},
toggleSeq() {
qbit.toggleSequentialDownload(this.hashes)
async toggleSeq() {
await qbit.toggleSequentialDownload(this.hashes)
},
toggleFL() {
qbit.toggleFirstLastPiecePriority(this.hashes)
async toggleFL() {
await qbit.toggleFirstLastPiecePriority(this.hashes)
},
toggleAutoTMM() {
qbit.setAutoTMM(this.hashes, !this.torrent.auto_tmm)
async toggleAutoTMM() {
await qbit.setAutoTMM(this.hashes, !this.torrent.auto_tmm)
},
copyToClipBoard(text) {
if (navigator.clipboard && window.isSecureContext) {

View file

@ -175,7 +175,7 @@ export default {
item.newName = item.name
this.toggleEditing(item)
},
renameFile(item) {
async renameFile(item) {
const lastPathSep = item.fullName.lastIndexOf('/')
const args = [this.hash]
@ -185,12 +185,12 @@ export default {
args.push(`${prefix}/${item.name}`, `${prefix}/${item.newName}`)
}
qbit.renameFile(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFileFailed')))
await qbit.renameFile(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFileFailed')))
item.name = item.newName
this.toggleEditing(item)
},
renameFolder(item) {
async renameFolder(item) {
const lastPathSep = item.fullName.lastIndexOf('/')
const args = [this.hash]
@ -200,13 +200,13 @@ export default {
args.push(`${prefix}/${item.name}`, `${prefix}/${item.newName}`)
}
qbit.renameFolder(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFolderFailed')))
await qbit.renameFolder(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFolderFailed')))
item.name = item.newName
this.toggleEditing(item)
},
setFilePrio(fileId, priority) {
qbit.setTorrentFilePriority(this.hash, [fileId], priority).then(() => this.initFiles())
async setFilePrio(fileId, priority) {
await qbit.setTorrentFilePriority(this.hash, [fileId], priority).then(() => this.initFiles())
}
}
}

View file

@ -77,25 +77,25 @@ export default {
this.$store.commit('FETCH_TAGS')
},
methods: {
addTag(tag) {
async addTag(tag) {
if (this.activeTags.includes(this.availableTags.indexOf(tag))) {
return this.deleteTag(tag)
await this.deleteTag(tag)
}
return qbit.addTorrentTag([this.hash], [tag])
await qbit.addTorrentTag([this.hash], [tag])
},
deleteTag(tag) {
qbit.removeTorrentTag([this.hash], [tag])
async deleteTag(tag) {
await qbit.removeTorrentTag([this.hash], [tag])
},
setCategory(cat) {
async setCategory(cat) {
if (this.torrent.category === cat.name) {
return this.deleteCategory()
await this.deleteCategory()
}
return qbit.setCategory([this.hash], cat.name)
await qbit.setCategory([this.hash], cat.name)
},
deleteCategory() {
qbit.setCategory([this.hash], '')
async deleteCategory() {
await qbit.setCategory([this.hash], '')
}
}
}

View file

@ -107,6 +107,11 @@
"downloaded": "Downloaded",
"uploaded": "Uploaded"
},
"search": {
"title": "Search torrents",
"runNewSearch": "Search",
"stopSearch": "Stop"
},
"navbar": {
"currentSpeed": "Current Speed",
"alltimeTitle": "All-Time Stats",
@ -173,8 +178,13 @@
"matchingArticles": {
"title": "Matching RSS Articles"
},
"pluginManager": {
"title": "Plugin manager"
"searchPluginManager": {
"title": "Plugin manager",
"checkForUpdates": "Check for Updates",
"install": {
"title": "Install new plugin",
"label": "URL or file path"
}
},
"search": {
"title": "Search",

View file

@ -24,6 +24,11 @@ const router = new Router({
name: 'rss',
component: () => import('./views/RssArticles.vue')
},
{
path: '/search',
name: 'search',
component: () => import('./views/SearchEngine.vue')
},
{
path: '/torrent/:hash',
name: 'torrentDetail',

View file

@ -48,13 +48,10 @@ export class QBitApi {
async login(params: LoginPayload): Promise<string> {
const payload = new URLSearchParams(params)
const res = await this.axios
.post('/auth/login', payload, {
validateStatus: (status: number) => status === 200 || status === 403
})
.catch(err => console.log(err))
return res?.data
return this.axios.post('/auth/login', payload, { validateStatus: (status: number) => status === 200 || status === 403 }).then(
res => res.data,
err => console.log(err)
)
}
async getAuthenticationStatus(): Promise<boolean> {
@ -65,7 +62,7 @@ export class QBitApi {
}
async logout(): Promise<void> {
await this.axios.post('/auth/logout')
return this.axios.post('/auth/logout')
}
async getAppPreferences(): Promise<AppPreferences> {
@ -77,7 +74,7 @@ export class QBitApi {
json: JSON.stringify(params)
}
await this.execute('/app/setPreferences', data)
return this.execute('/app/setPreferences', data)
}
async getMainData(rid?: number): Promise<MainDataResponse> {
@ -85,7 +82,7 @@ export class QBitApi {
}
async toggleSpeedLimitsMode(): Promise<void> {
await this.execute('/transfer/toggleSpeedLimitsMode')
return this.execute('/transfer/toggleSpeedLimitsMode')
}
async getTorrents(payload: SortOptions): Promise<Torrent[]> {
@ -122,7 +119,7 @@ export class QBitApi {
}
async setTorrentName(hash: string, name: string): Promise<void> {
await this.execute('/torrents/rename', { hash, name })
return this.execute('/torrents/rename', { hash, name })
}
async getTorrentPieceStates(hash: string): Promise<number[]> {
@ -156,7 +153,7 @@ export class QBitApi {
// RSS
async createFeed(payload: CreateFeedPayload): Promise<void> {
await this.execute('/rss/addFeed', {
return this.execute('/rss/addFeed', {
url: payload.url,
path: payload.name
})
@ -178,40 +175,38 @@ export class QBitApi {
}
async editFeed(itemPath: string, destPath: string): Promise<void> {
await this.execute('/rss/moveItem', {
return this.execute('/rss/moveItem', {
itemPath,
destPath
})
}
async renameRule(ruleName: string, newRuleName: string): Promise<void> {
await this.execute('/rss/renameRule', {
return this.execute('/rss/renameRule', {
ruleName,
newRuleName
})
}
async deleteRule(ruleName: string): Promise<void> {
await this.execute('rss/removeRule', {
ruleName
})
return this.execute('rss/removeRule', { ruleName })
}
async deleteFeed(name: string): Promise<void> {
await this.execute('rss/removeItem', {
return this.execute('rss/removeItem', {
path: name
})
}
async markAsRead(itemPath: string, articleId: string) {
await this.execute('rss/markAsRead', {
return this.execute('rss/markAsRead', {
itemPath,
articleId
})
}
async refreshFeed(itemPath: string) {
await this.execute('rss/refreshItem', {
return this.execute('rss/refreshItem', {
itemPath
})
}
@ -243,7 +238,7 @@ export class QBitApi {
data = new URLSearchParams(params as Parameters)
}
await this.axios.post('/torrents/add', data)
return this.axios.post('/torrents/add', data)
}
async setTorrentFilePriority(hash: string, idList: number[], priority: Priority): Promise<void> {
@ -253,45 +248,45 @@ export class QBitApi {
priority
}
await this.execute('/torrents/filePrio', params)
return this.execute('/torrents/filePrio', params)
}
async deleteTorrents(hashes: string[], deleteFiles: boolean): Promise<void> {
if (!hashes.length) return
await this.torrentAction('delete', hashes, { deleteFiles })
return this.torrentAction('delete', hashes, { deleteFiles })
}
async pauseTorrents(hashes: string[]): Promise<void> {
await this.torrentAction('pause', hashes)
return this.torrentAction('pause', hashes)
}
async resumeTorrents(hashes: string[]): Promise<void> {
await this.torrentAction('resume', hashes)
return this.torrentAction('resume', hashes)
}
async forceStartTorrents(hashes: string[]): Promise<void> {
await this.torrentAction('setForceStart', hashes, { value: true })
return this.torrentAction('setForceStart', hashes, { value: true })
}
async toggleSequentialDownload(hashes: string[]): Promise<void> {
await this.torrentAction('toggleSequentialDownload', hashes)
return this.torrentAction('toggleSequentialDownload', hashes)
}
async toggleFirstLastPiecePriority(hashes: string[]): Promise<void> {
await this.torrentAction('toggleFirstLastPiecePrio', hashes)
return this.torrentAction('toggleFirstLastPiecePrio', hashes)
}
async setAutoTMM(hashes: string[], enable: boolean): Promise<void> {
await this.torrentAction('setAutoManagement', hashes, { enable })
return this.torrentAction('setAutoManagement', hashes, { enable })
}
async setDownloadLimit(hashes: string[], limit: number): Promise<void> {
await this.torrentAction('setDownloadLimit', hashes, { limit })
return this.torrentAction('setDownloadLimit', hashes, { limit })
}
async setUploadLimit(hashes: string[], limit: number): Promise<void> {
await this.torrentAction('setUploadLimit', hashes, { limit })
return this.torrentAction('setUploadLimit', hashes, { limit })
}
/**
@ -316,7 +311,7 @@ export class QBitApi {
limit
}
await this.execute('/transfer/setDownloadLimit', data)
return this.execute('/transfer/setDownloadLimit', data)
}
/**
@ -327,26 +322,26 @@ export class QBitApi {
limit
}
await this.execute('/transfer/setUploadLimit', data)
return this.execute('/transfer/setUploadLimit', data)
}
async setShareLimit(hashes: string[], ratioLimit: number, seedingTimeLimit: number): Promise<void> {
await this.torrentAction('setShareLimits', hashes, {
return this.torrentAction('setShareLimits', hashes, {
ratioLimit,
seedingTimeLimit
})
}
async reannounceTorrents(hashes: string[]): Promise<void> {
await this.torrentAction('reannounce', hashes)
return this.torrentAction('reannounce', hashes)
}
async recheckTorrents(hashes: string[]): Promise<void> {
await this.torrentAction('recheck', hashes)
return this.torrentAction('recheck', hashes)
}
async setTorrentLocation(hashes: string[], location: string): Promise<void> {
await this.torrentAction('setLocation', hashes, { location })
return this.torrentAction('setLocation', hashes, { location })
}
async addTorrentTrackers(hash: string, trackers: string): Promise<void> {
@ -355,7 +350,7 @@ export class QBitApi {
urls: trackers
}
await this.execute(`/torrents/addTrackers`, params)
return this.execute(`/torrents/addTrackers`, params)
}
async removeTorrentTrackers(hash: string, trackers: string[]): Promise<void> {
@ -364,11 +359,11 @@ export class QBitApi {
urls: trackers.join('|')
}
await this.execute(`/torrents/removeTrackers`, params)
return this.execute(`/torrents/removeTrackers`, params)
}
async addTorrentPeers(hashes: string[], peers: string[]): Promise<void> {
await this.torrentAction('addPeers', hashes, { peers: peers.join('|') })
return this.torrentAction('addPeers', hashes, { peers: peers.join('|') })
}
async banPeers(peers: string[]): Promise<void> {
@ -376,7 +371,7 @@ export class QBitApi {
peers: peers.join('|')
}
await this.execute('/transfer/banPeers', params)
return this.execute('/transfer/banPeers', params)
}
async torrentAction(action: string, hashes: string[], extra?: Record<string, any>): Promise<any> {
@ -395,7 +390,7 @@ export class QBitApi {
newPath
}
await this.execute('/torrents/renameFile', params)
return this.execute('/torrents/renameFile', params)
}
async renameFolder(hash: string, oldPath: string, newPath: string): Promise<void> {
@ -405,33 +400,33 @@ export class QBitApi {
newPath
}
await this.execute('/torrents/renameFolder', params)
return this.execute('/torrents/renameFolder', params)
}
/** Torrent Priority **/
async setTorrentPriority(hashes: string[], priority: 'increasePrio' | 'decreasePrio' | 'topPrio' | 'bottomPrio'): Promise<void> {
await this.execute(`/torrents/${priority}`, {
return this.execute(`/torrents/${priority}`, {
hashes: hashes.join('|')
})
}
/** Begin Torrent Tags **/
async removeTorrentTag(hashes: string[], tags: string[]): Promise<void> {
await this.torrentAction('removeTags', hashes, { tags: tags.join('|') })
return this.torrentAction('removeTags', hashes, { tags: tags.join('|') })
}
async addTorrentTag(hashes: string[], tags: string[]): Promise<void> {
await this.torrentAction('addTags', hashes, { tags: tags.join('|') })
return this.torrentAction('addTags', hashes, { tags: tags.join('|') })
}
async createTag(tags: string[]): Promise<void> {
await this.execute('/torrents/createTags', {
return this.execute('/torrents/createTags', {
tags: tags.join(',')
})
}
async deleteTag(tags: string[]): Promise<void> {
await this.execute('/torrents/deleteTags', {
return this.execute('/torrents/deleteTags', {
tags: tags.join(',')
})
}
@ -445,20 +440,20 @@ export class QBitApi {
}
async deleteCategory(categories: string[]): Promise<void> {
await this.execute('/torrents/removeCategories', {
return this.execute('/torrents/removeCategories', {
categories: categories.join('\n')
})
}
async createCategory(cat: Category): Promise<void> {
await this.execute('/torrents/createCategory', {
return this.execute('/torrents/createCategory', {
category: cat.name,
savePath: cat.savePath
})
}
async setCategory(hashes: string[], category: string): Promise<void> {
await this.torrentAction('setCategory', hashes, { category })
return this.torrentAction('setCategory', hashes, { category })
}
async editCategory(cat: Category): Promise<void> {
@ -467,7 +462,7 @@ export class QBitApi {
savePath: cat.savePath
}
await this.execute('/torrents/editCategory', params)
return this.execute('/torrents/editCategory', params)
}
async exportTorrent(hash: string): Promise<Blob> {
@ -483,40 +478,27 @@ export class QBitApi {
}
/** Search **/
async getSearchPlugins(): Promise<SearchPlugin[]> {
return this.axios.get('/search/plugins').then(res => res.data)
}
async updateSearchPlugins(): Promise<void> {
await this.execute('/search/updatePlugins')
}
async enableSearchPlugin(pluginNames: string[], enable: boolean): Promise<void> {
const params = {
names: pluginNames.join('|'),
enable
}
await this.execute('/search/enablePlugin', params)
}
async startSearch(pattern: string, plugins: string[]): Promise<SearchJob> {
async startSearch(pattern: string, category: string, plugins: string[]): Promise<SearchJob> {
const params = {
pattern,
plugins: plugins.length ? plugins.join('|') : 'enabled',
category: 'all'
category,
plugins: plugins.join('|')
}
return this.execute('/search/start', params)
}
async stopSearch(id: number): Promise<void> {
await this.execute('/search/stop', { id })
async stopSearch(id: number): Promise<boolean> {
return this.execute('/search/stop', { id }).then(
() => true,
() => false
)
}
async getSearchStatus(id?: number): Promise<SearchStatus[]> {
const params = id !== undefined ? { id } : undefined
return this.execute('/search/status', params)
return this.execute('/search/status', {
id: id !== undefined ? id : 0
}).then(res => res.data)
}
async getSearchResults(id: number, limit?: number, offset?: number): Promise<SearchResultsResponse> {
@ -527,6 +509,41 @@ export class QBitApi {
})
}
async deleteSearchPlugin(id: number): Promise<boolean> {
return this.execute('/search/delete', { id }).then(
() => true,
() => false
)
}
async getSearchPlugins(): Promise<SearchPlugin[]> {
return this.axios.get('/search/plugins').then(res => res.data)
}
async installSearchPlugin(sources: string[]) {
return this.execute('/search/installPlugin', { sources: sources.join('|') }).then(
() => true,
() => false
)
}
async uninstallSearchPlugin(names: string[]) {
return this.execute('/search/uninstallPlugin', { names: names.join('|') })
}
async enableSearchPlugin(names: string[], enable: boolean): Promise<void> {
const params = {
names: names.join('|'),
enable
}
return this.execute('/search/enablePlugin', params)
}
async updateSearchPlugins(): Promise<void> {
return this.execute('/search/updatePlugins')
}
async shutdownApp(): Promise<boolean> {
return this.axios
.post('/app/shutdown')

View file

@ -39,5 +39,8 @@ export default {
store.state.oldSettingsDetected = true
Vue.$toast.error(i18n.t('toast.resetSettingsNeeded').toString(), { timeout: 2500 })
},
FETCH_SEARCH_PLUGINS: async (store: Store<StoreState>) => {
await qbit.getSearchPlugins().then(plugins => store.commit('UPDATE_SEARCH_PLUGINS', plugins))
}
}

View file

@ -3,7 +3,7 @@ import { DocumentTitle, Tags, Trackers, Torrents, Graph } from '@/actions'
import { setLanguage } from '@/plugins/i18n'
import type { ModalTemplate, StoreState } from '@/types/vuetorrent'
import Torrent from '@/models/Torrent'
import type { AppPreferences } from '@/types/qbit/models'
import type { AppPreferences, SearchPlugin } from '@/types/qbit/models'
import { Status } from '@/models'
import router from '@/router'
@ -94,7 +94,7 @@ export default {
FETCH_TAGS: async (state: StoreState) => (state.tags = await qbit.getAvailableTags()),
FETCH_FEEDS: async (state: StoreState) => (state.rss.feeds = Object.entries(await qbit.getFeeds(true)).map(([key, value]) => ({ name: key, ...value }))),
FETCH_RULES: async (state: StoreState) => (state.rss.rules = Object.entries(await qbit.getRules()).map(([key, value]) => ({ name: key, ...value }))),
FETCH_SEARCH_PLUGINS: async (state: StoreState) => (state.searchPlugins = await qbit.getSearchPlugins()),
UPDATE_SEARCH_PLUGINS: async (state: StoreState, plugins: SearchPlugin[]) => (state.searchPlugins = plugins),
SET_CURRENT_ITEM_COUNT: (state: StoreState, count: number) => (state.filteredTorrentsCount = count),
SET_LANGUAGE: async (state: StoreState) => setLanguage(state.webuiSettings.lang)
}

View file

@ -3,6 +3,6 @@ export default interface SearchStatus {
id: number
/** Current status of the search job (either Running or Stopped) */
status: 'Running' | 'Stopped'
/** Total number of results. If the status is Running this number may contineu to increase */
/** Total number of results. If the status is Running this number may continue to increase */
total: number
}

View file

@ -81,13 +81,10 @@ export default defineComponent({
headers: [
{ text: this.$t('modals.rss.columnTitle.id'), value: 'id' },
{ text: this.$t('modals.rss.columnTitle.title'), value: 'title' },
// {text: this.$t('modals.rss.columnTitle.description'), value: 'description'},
{ text: this.$t('modals.rss.columnTitle.category'), value: 'category' },
{ text: this.$t('modals.rss.columnTitle.author'), value: 'author' },
{ text: this.$t('modals.rss.columnTitle.date'), value: 'parsedDate' },
{ text: this.$t('modals.rss.columnTitle.feedName'), value: 'feedName' },
// {text: this.$t('modals.rss.columnTitle.link'), value: 'link'},
// {text: this.$t('modals.rss.columnTitle.torrentURL'), value: 'torrentURL'},
{ text: this.$t('modals.rss.columnTitle.actions'), value: 'actions', sortable: false }
],
filter: '',

178
src/views/SearchEngine.vue Normal file
View file

@ -0,0 +1,178 @@
<template>
<div class="px-1 px-sm-5 background noselect">
<v-row no-gutters class="grey--text" align="center" justify="center">
<v-col>
<h1 style="font-size: 1.6em !important" class="subtitle-1 ml-2">
{{ $t('search.title') }}
</h1>
</v-col>
<v-col class="align-center justify-center">
<v-card-actions class="justify-end">
<v-btn small elevation="0" color="primary" @click="openPluginManager">
<v-icon>{{ mdiToyBrick }}</v-icon>
</v-btn>
<v-btn small elevation="0" @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-card-actions>
</v-col>
</v-row>
<v-card flat>
<v-list-item class="searchCriterias">
<v-row class="my-2">
<v-col cols="12" md="6">
<v-text-field v-model="searchPattern" dense hide-details clearable :rules="[v => !!v || 'Search term is required']" label="Search pattern" />
</v-col>
<v-col cols="6" sm="5" md="2">
<v-select v-model="searchCategory" height="1" flat dense hide-details outlined :items="categories" label="Search category" />
</v-col>
<v-col cols="6" sm="5" md="2">
<v-select v-model="searchPlugin" height="1" flat dense hide-details outlined :items="plugins" label="Search plugins" />
</v-col>
<v-col cols="12" sm="2" class="d-flex align-center justify-center">
<v-btn v-if="queryId === 0" class="mx-auto accent white--text elevation-0 px-4" @click="runNewSearch">
{{ $t('search.runNewSearch') }}
</v-btn>
<v-btn v-else class="mx-auto warning white--text elevation-0 px-4" @click="stopSearch">
{{ $t('search.stopSearch') }}
</v-btn>
</v-col>
</v-row>
</v-list-item>
</v-card>
<v-card flat class="mt-5">
<v-list-item class="searchResults">
<v-data-table style="width: 100%" id="searchResultsTable" :headers="headers" :items="filteredResults" :search="resultFilter">
<template v-slot:top>
<v-row class="mt-2">
<v-col cols="12" md="6">
<v-text-field v-model="resultFilter" dense hide-details label="Filter" />
</v-col>
</v-row>
</template>
<template v-slot:item.fileSize="{ item }">
{{ item.fileSize | formatSize }}
</template>
<template v-slot:item.actions="{ item }">
<span class="search-actions">
<v-icon @click="downloadTorrent(item)">{{ mdiDownload }}</v-icon>
</span>
</template>
</v-data-table>
</v-list-item>
</v-card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { mapState } from 'vuex'
import { mdiClose, mdiDownload, mdiToyBrick } from '@mdi/js'
import qbit from '@/services/qbit'
import { General } from '@/mixins'
import { SearchPlugin, SearchResult } from '@/types/qbit/models'
export default defineComponent({
name: 'SearchEngine',
mixins: [General],
data() {
return {
headers: [
{ text: 'Filename', value: 'fileName' },
{ text: 'File Size', value: 'fileSize' },
{ text: 'Seeders', value: 'nbSeeders' },
{ text: 'Leechers', value: 'nbLeechers' },
{ text: 'Site URL', value: 'siteUrl' },
{ text: '', value: 'actions', sortable: false }
],
searchPattern: '',
searchCategory: 'all',
searchPlugin: 'enabled',
queryId: 0,
queryTimer: NaN as NodeJS.Timer,
queryResults: [] as SearchResult[],
resultFilter: '',
mdiClose,
mdiDownload,
mdiToyBrick
}
},
computed: {
...mapState(['searchPlugins']),
categories() {
const cats = [
{ text: 'Movies', value: 'movies' },
{ text: 'TV shows', value: 'tv' },
{ text: 'Music', value: 'music' },
{ text: 'Games', value: 'games' },
{ text: 'Anime', value: 'anime' },
{ text: 'Software', value: 'software' },
{ text: 'Pictures', value: 'pictures' },
{ text: 'Books', value: 'books' }
]
cats.sort((a, b) => a.text.localeCompare(b.text))
return [{ text: 'All categories', value: 'all' }, ...cats]
},
plugins() {
const plugins = [
{ text: 'All plugins', value: 'all' },
{ text: 'Only enabled', value: 'enabled' }
]
this.searchPlugins.filter((plugin: SearchPlugin) => plugin.enabled).forEach((plugin: SearchPlugin) => plugins.push({ text: plugin.fullName, value: plugin.name }))
return plugins
},
filteredResults() {
return this.queryResults
}
},
async mounted() {
await this.$store.dispatch('FETCH_SEARCH_PLUGINS')
},
async beforeDestroy() {
await this.stopSearch()
},
methods: {
openPluginManager() {
this.createModal('SearchPluginManager')
},
close() {
this.$router.back()
},
async runNewSearch() {
const searchJob = await qbit.startSearch(this.searchPattern, this.searchCategory, [this.searchPlugin])
this.queryId = searchJob.id
this.queryTimer = setInterval(() => this.refreshResults(), 1000)
},
async stopSearch() {
if (this.queryId !== 0) await qbit.stopSearch(this.queryId)
this.queryId = 0
clearInterval(this.queryTimer)
},
async refreshResults() {
const response = await qbit.getSearchResults(this.queryId, 100, this.queryResults.length)
this.queryResults.push(...response.results)
if (response.status === 'Stopped') {
this.queryId = 0
await this.stopSearch()
}
},
downloadTorrent(item: SearchResult) {
this.createModal('AddModal', { initialMagnet: item.fileUrl })
}
}
})
</script>
<style scoped lang="scss">
.search-actions {
display: flex;
flex-direction: row;
}
</style>