Merge pull request #451 from WDaan/feat/rss

feat: basic rss interface (#447)
This commit is contained in:
Daan Wijns 2022-07-04 11:37:25 +02:00 committed by GitHub
commit 04cd58c693
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 575 additions and 49 deletions

3
.gitignore vendored
View file

@ -2,6 +2,9 @@
node_modules
/dist
# asdf config
.tool-versions
# local env files
.env.local
.env.*.local

View file

@ -0,0 +1,95 @@
<template>
<v-dialog v-model="dialog" content-class="rounded-form" max-width="300px">
<v-card>
<v-card-title class="pa-0">
<v-toolbar-title class="ma-4 primarytext--text">
<h3>{{ hasInitialFeed ? $t('edit') : $t('createNew') }} {{ $t('feed') }}</h3>
</v-toolbar-title>
</v-card-title>
<v-card-text>
<v-form ref="feedForm" class="px-6 mt-3">
<v-container>
<v-text-field
v-model="feed.url"
:label="$t('modals.newFeed.url')"
required
/>
</v-container>
</v-form>
</v-card-text>
<v-divider />
<v-card-actions class="justify-end">
<v-btn
v-if="!hasInitialFeed"
class="accent white--text elevation-0 px-4"
@click="create"
>
{{ $t('create') }}
</v-btn>
<v-btn
v-else
class="accent white--text elevation-0 px-4"
@click="edit"
>
{{ $t('edit') }}
</v-btn>
<v-btn
class="error white--text elevation-0 px-4"
@click="cancel"
>
{{ $t('cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
import { mdiCancel, mdiTagPlus, mdiPencil } from '@mdi/js'
import Vue from 'vue'
export default {
name: 'FeedForm',
mixins: [Modal],
props: {
initialFeed: Object
},
data: () => ({
feed: { url: '' },
mdiCancel, mdiTagPlus, mdiPencil
}),
computed: {
...mapGetters(['getSelectedFeed']),
hasInitialFeed() {
return !!(this.initialFeed &&
this.initialFeed.name)
}
},
created() {
this.$store.commit('FETCH_FEEDS')
if (this.hasInitialFeed) {
this.feed = this.initialFeed
}
},
methods: {
create() {
qbit.createFeed(this.feed)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_FEEDS')
this.dialog = false
},
edit() {
qbit.editfeed(this.feed)
Vue.$toast.success(this.$t('toast.feedSaved'))
this.cancel()
}
}
}
</script>
<style></style>

View file

@ -0,0 +1,123 @@
<template>
<v-dialog v-model="dialog" max-width="300px">
<v-card flat>
<v-card-title class="pa-0">
<v-toolbar-title class="ma-4 primarytext--text">
<h3>{{ hasInitialRule ? $t('edit') : $t('createNew') }} {{ $t('rule') }}</h3>
</v-toolbar-title>
</v-card-title>
<v-card-text>
<v-form ref="ruleForm" class="px-6 mt-3">
<v-container>
<v-text-field
v-model="rule.name"
:label="$t('modals.newRule.name')"
required
/>
</v-container>
<v-container>
<v-text-field
v-model="rule.def.mustContain"
:label="$t('modals.newRule.def.mustContain')"
required
/>
</v-container>
<v-container>
<v-subheader>{{ $t('modals.newRule.def.affectedFeeds') }}</v-subheader>
<template v-for="(item, index) in availableFeeds">
<v-checkbox
:key="index"
v-model="rule.def.affectedFeeds"
:label="item.name"
:value="item.url"
/>
</template>
</v-container>
</v-form>
</v-card-text>
<v-divider />
<v-card-actions class="justify-end">
<v-btn
v-if="!hasInitialRule"
class="accent white--text elevation-0 px-4"
@click="create"
>
{{ $t('create') }}
</v-btn>
<v-btn
v-else
class="accent white--text elevation-0 px-4"
@click="edit"
>
{{ $t('edit') }}
</v-btn>
<v-btn
class="error white--text elevation-0 px-4"
@click="cancel"
>
{{ $t('cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { Modal } from '@/mixins'
import { mdiCancel, mdiTagPlus, mdiPencil } from '@mdi/js'
import Vue from 'vue'
export default {
name: 'RuleForm',
mixins: [Modal],
props: {
initialRule: Object
},
data: () => ({
rule: {
name: '',
def: {
mustContain: '',
affectedFeeds: [],
enabled: true
}
},
mdiCancel, mdiTagPlus, mdiPencil
}),
computed: {
...mapGetters(['getSelectedRule', 'getFeeds']),
availableFeeds() {
return this.getFeeds()
},
hasInitialRule() {
return !!(this.initialRule &&
this.initialRule.name)
}
},
created() {
this.$store.commit('FETCH_RULES')
if (this.hasInitialRule) {
this.rule = this.initialRule
}
},
methods: {
create() {
qbit.createRule(this.rule.name, this.rule.def)
this.cancel()
},
cancel() {
this.$store.commit('FETCH_RULES')
this.dialog = false
},
edit() {
qbit.editRule(this.rule)
Vue.$toast.success(this.$t('toast.ruleSaved'))
this.cancel()
}
}
}
</script>
<style></style>

View file

@ -0,0 +1,4 @@
import FeedForm from './FeedForm.vue'
import RuleForm from './RuleForm.vue'
export { FeedForm, RuleForm }

View file

@ -0,0 +1,44 @@
<template>
<v-card flat>
<v-tabs v-model="tab">
<v-tab href="#general">
{{ $t('modals.settings.pageRss.tabName.general') }}
</v-tab>
<v-tab href="#feeds">
{{ $t('modals.settings.pageRss.tabName.feeds') }}
</v-tab>
<v-tab href="#rules">
{{ $t('modals.settings.pageRss.tabName.rules') }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab" touchless>
<v-tab-item eager value="general">
<General />
</v-tab-item>
<v-tab-item eager value="feeds">
<Feeds />
</v-tab-item>
<v-tab-item eager value="rules">
<Rules />
</v-tab-item>
</v-tabs-items>
</v-card>
</template>
<script>
import General from './Rss/General'
import Feeds from './Rss/Feeds'
import Rules from './Rss/Rules'
import { FullScreenModal } from '@/mixins'
export default {
name: 'VueTorrent',
components: {
General, Feeds, Rules
},
mixins: [FullScreenModal],
data: () => ({
tab: null
})
}
</script>

View file

@ -0,0 +1,69 @@
<template>
<v-card flat>
<v-row dense class="ma-0 pa-0">
<v-col cols="12" md="6">
<v-subheader>{{ $t('modals.settings.pageRss.pageFeeds.feeds') }}</v-subheader>
<template v-for="(item, index) in availableFeeds">
<v-list-item :key="item.uid">
<v-list-item-content>
<v-list-item-title v-text="item.name" />
</v-list-item-content>
<v-list-item-action>
<v-icon color="red" @click="deleteFeed(item)">
{{ mdiDelete }}
</v-icon>
</v-list-item-action>
</v-list-item>
<v-divider
v-if="index < availableFeeds.length - 1"
:key="index"
/>
</template>
<v-list-item>
<v-btn
class="mx-auto accent white--text elevation-0 px-4"
@click="createFeed"
>
{{ $t('modals.settings.pageRss.pageFeeds.btnCreateNew') }}
</v-btn>
</v-list-item>
</v-col>
</v-row>
</v-card>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { mdiDelete } from '@mdi/js'
import { Tab, General, FullScreenModal } from '@/mixins'
export default {
name: 'Feeds',
mixins: [Tab, General, FullScreenModal],
data: () => ({
mdiDelete
}),
computed: {
...mapGetters(['getFeeds']),
availableFeeds() {
return this.getFeeds()
}
},
created() {
this.$store.commit('FETCH_FEEDS')
},
methods: {
activeMethod() {
this.$store.commit('FETCH_FEEDS')
},
deleteFeed(item) {
qbit.deleteFeed(item.name)
this.$store.commit('FETCH_FEEDS')
},
createFeed() {
this.createModal('FeedForm')
}
}
}
</script>

View file

@ -0,0 +1,30 @@
<template>
<v-card flat>
<v-subheader>{{ $t('modals.settings.pageRss.pageGeneral.rssAutoProcessing') }}</v-subheader>
<v-list-item>
<v-checkbox
v-model="settings.rss_processing_enabled"
hide-details
class="ma-0 pa-0"
:label="$t('modals.settings.pageRss.pageGeneral.input.enableRssProcessing')"
/>
</v-list-item>
<v-subheader>{{ $t('modals.settings.pageRss.pageGeneral.rssAutoDownloader') }}</v-subheader>
<v-list-item>
<v-checkbox
v-model="settings.rss_auto_downloading_enabled"
hide-details
class="ma-0 pa-0"
:label="$t('modals.settings.pageRss.pageGeneral.input.enableRssAutoDownload')"
/>
</v-list-item>
</v-card>
</template>
<script>
import { FullScreenModal, SettingsTab } from '@/mixins'
export default {
name: 'Rss',
mixins: [SettingsTab, FullScreenModal]
}
</script>

View file

@ -0,0 +1,69 @@
<template>
<v-card flat>
<v-row dense class="ma-0 pa-0">
<v-col cols="12" md="6">
<v-subheader>{{ $t('modals.settings.pageRss.pageRules.rules') }}</v-subheader>
<template v-for="(item, index) in availableRules">
<v-list-item :key="item.uid">
<v-list-item-content>
<v-list-item-title v-text="item.name" />
</v-list-item-content>
<v-list-item-action>
<v-icon color="red" @click="deleteRule(item)">
{{ mdiDelete }}
</v-icon>
</v-list-item-action>
</v-list-item>
<v-divider
v-if="index < availableRules.length - 1"
:key="index"
/>
</template>
<v-list-item>
<v-btn
class="mx-auto accent white--text elevation-0 px-4"
@click="createRule"
>
{{ $t('modals.settings.pageRss.pageRules.btnCreateNew') }}
</v-btn>
</v-list-item>
</v-col>
</v-row>
</v-card>
</template>
<script>
import { mapGetters } from 'vuex'
import qbit from '@/services/qbit'
import { mdiDelete } from '@mdi/js'
import { Tab, General, FullScreenModal } from '@/mixins'
export default {
name: 'Rules',
mixins: [Tab, General, FullScreenModal],
data: () => ({
mdiDelete
}),
computed: {
...mapGetters(['getRules']),
availableRules() {
return this.getRules()
}
},
created() {
this.$store.commit('FETCH_RULES')
},
methods: {
activeMethod() {
this.$store.commit('FETCH_RULES')
},
deleteRule(item) {
qbit.deleteRule(item.name)
this.$store.commit('FETCH_RULES')
},
createRule() {
this.createModal('RuleForm')
}
}
}
</script>

View file

@ -4,5 +4,6 @@ import Downloads from './Downloads.vue'
import VueTorrent from './VueTorrent.vue'
import TagsAndCategories from './TagsAndCategories.vue'
import Connection from './Connection.vue'
import Rss from './Rss.vue'
export { WebUI, BitTorrent, Downloads, VueTorrent, TagsAndCategories, Connection }
export { WebUI, BitTorrent, Downloads, VueTorrent, TagsAndCategories, Connection, Rss }

View file

@ -84,6 +84,17 @@ const locale = {
/** Modals */
modals: {
newFeed: {
feedName: 'Name',
url: 'URL'
},
newRule: {
name: 'Name',
def: {
mustContain: 'Must Contain',
affectedFeeds: 'Apply Rule to Feeds'
}
},
pluginManager: {
title: 'Plugin manager'
},
@ -106,6 +117,7 @@ const locale = {
downloads: 'downloads',
connection: 'connection',
bittorrent: 'bittorrent',
rss: 'Rss',
webUI: 'WEB UI',
tagsAndCategories: 'tags & categories'
},
@ -191,6 +203,29 @@ const locale = {
whenRatioReaches: 'When ratio reaches',
whenSeedingTimeReaches: 'When seeding time reaches'
},
pageRss: {
tabName: {
general: 'General',
feeds: 'Feeds',
rules: 'Rules'
},
pageRules: {
rules: 'Rules',
btnCreateNew: 'Create Rule'
},
pageFeeds: {
feeds: 'Feeds',
btnCreateNew: 'Add feed'
},
pageGeneral: {
rssAutoProcessing: 'RSS Reader',
rssAutoDownloader: 'RSS Torrent Auto Downloader',
input: {
enableRssAutoDownload: 'Enable auto downloading of RSS torrents',
enableRssProcessing: 'Enable fetching RSS feeds'
}
}
},
pageWebUI: {
useAlternativeWebUI: 'Use Alternative WebUI',
filesLocation: 'Files location',
@ -345,7 +380,9 @@ const locale = {
loginSuccess: 'Successfully logged in! 🎉',
loginFailed: 'Login failed 😕',
settingsSaved: 'Settings saved successfully!',
categorySaved: 'Category edited successfully!'
categorySaved: 'Category edited successfully!',
feedSaved: 'Feed saved successfully!',
ruleSaved: 'Rule saved!'
},
/** RightClick **/

View file

@ -154,6 +154,52 @@ class Qbit {
}).then(res => res.data)
}
// RSS
createFeed(feed) {
return this.execute('post', '/rss/addFeed', {
url: feed.url,
path: feed.url
})
}
createRule(ruleName, defs) {
return this.execute('post', '/rss/setRule', {
ruleName: ruleName,
ruleDef: JSON.stringify(defs)
})
}
getFeeds() {
return this.axios.get('/rss/items')
.then(res => res.data)
.then(data =>
Object.entries(data).map(feed => {
return { name: feed[0], ...feed[1] }
}))
}
getRules() {
return this.axios.get('/rss/rules')
.then(res => res.data)
.then(data =>
Object.entries(data).map(rule => {
return { name: rule[0], ...rule[1] }
}))
}
deleteRule(ruleName) {
return this.execute('post', 'rss/removeRule', {
ruleName
})
}
deleteFeed(name) {
return this.execute('post', 'rss/removeItem', {
path: name
})
}
// Post
addTorrents(params, torrents) {

View file

@ -11,6 +11,8 @@ export default {
getWebuiSettings: state => () => state.webuiSettings,
getAvailableTags: state => () => state.tags,
getCategories: state => () => state.categories,
getFeeds: state => () => state.rss.feeds,
getRules: state => () => state.rss.rules,
getModals: state => () => state.modals,
getTorrents: state => () => state.torrents,
getTrackers: state => () => state.trackers,

View file

@ -22,6 +22,10 @@ export default new Vuex.Store({
state: {
version: 0,
intervals: [],
rss: {
feeds: [],
rules: []
},
status: {
status: '',
downloaded: '',

View file

@ -80,6 +80,8 @@ export default {
state.sort_options.tracker = tracker
},
FETCH_CATEGORIES: async state => state.categories = Object.values(await (qbit.getCategories())),
FETCH_FEEDS: async state => state.rss.feeds = await qbit.getFeeds(),
FETCH_RULES: async state => state.rss.rules = await qbit.getRules(),
FETCH_SEARCH_PLUGINS: async state => state.searchPlugins = await qbit.getSearchPlugins(),
SET_CURRENT_ITEM_COUNT: (state, count) => (state.filteredTorrentsCount = count),
SET_LANGUAGE: async state => await loadLanguageAsync(state.webuiSettings.lang)

View file

@ -1,33 +1,17 @@
<template>
<div
class="px-1 px-sm-5 background noselect"
>
<v-row
no-gutters
class="grey--text"
align="center"
justify="center"
>
<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('settings') | titleCase }}
{{ $t("settings") | titleCase }}
</h1>
</v-col>
<v-col class="align-center justify-center">
<v-card-actions class="justify-end">
<v-btn
class="accent"
small
elevation="0"
@click="saveSettings"
>
<v-btn class="accent" small elevation="0" @click="saveSettings">
<v-icon>{{ mdiContentSave }}</v-icon>
</v-btn>
<v-btn
small
elevation="0"
@click="close"
>
<v-btn small elevation="0" @click="close">
<v-icon>{{ mdiClose }}</v-icon>
</v-btn>
</v-card-actions>
@ -42,26 +26,28 @@
background-color="primary"
>
<v-tab class="white--text" href="#vuetorrent">
<h4>{{ $t('modals.settings.tabName.VueTorrent') }}</h4>
<h4>{{ $t("modals.settings.tabName.VueTorrent") }}</h4>
</v-tab>
<v-tab class="white--text" href="#downloads">
<h4>{{ $t('modals.settings.tabName.downloads') }}</h4>
<h4>{{ $t("modals.settings.tabName.downloads") }}</h4>
</v-tab>
<v-tab class="white--text" href="#connection">
<h4>{{ $t('modals.settings.tabName.connection') }}</h4>
<h4>{{ $t("modals.settings.tabName.connection") }}</h4>
</v-tab>
<v-tab class="white--text" href="#bittorrent">
<h4>{{ $t('modals.settings.tabName.bittorrent') }}</h4>
<h4>{{ $t("modals.settings.tabName.bittorrent") }}</h4>
</v-tab>
<v-tab class="white--text" href="#rss">
<h4>{{ $t("modals.settings.tabName.rss") }}</h4>
</v-tab>
<v-tab class="white--text" href="#webui">
<h4>{{ $t('modals.settings.tabName.webUI') }}</h4>
<h4>{{ $t("modals.settings.tabName.webUI") }}</h4>
</v-tab>
<v-tab class="white--text" href="#tagsAndCategories">
<h4>{{ $t('modals.settings.tabName.tagsAndCategories') }}</h4>
<h4>{{ $t("modals.settings.tabName.tagsAndCategories") }}</h4>
</v-tab>
</v-tabs>
<!--<v-divider />-->
<v-card-text class="pa-0">
<v-tabs-items v-model="tab" touchless>
@ -77,6 +63,9 @@
<v-tab-item eager value="bittorrent">
<BitTorrent :is-active="tab === 'bittorrent'" />
</v-tab-item>
<v-tab-item eager value="rss">
<Rss :is-active="tab === 'rss'" />
</v-tab-item>
<v-tab-item eager value="webui">
<WebUI :is-active="tab === 'webui'" />
</v-tab-item>
@ -90,46 +79,54 @@
</template>
<script>
import { mapGetters } from 'vuex'
import { mdiClose, mdiContentSave } from '@mdi/js'
import { mapGetters } from "vuex";
import { mdiClose, mdiContentSave } from "@mdi/js";
import {
WebUI,
BitTorrent,
Downloads,
VueTorrent,
TagsAndCategories,
Connection
} from '@/components/Settings/Tabs'
import { SettingsTab } from '../mixins'
Connection,
} from "@/components/Settings/Tabs";
import { SettingsTab } from "../mixins";
export default {
name: 'Settings',
components: { WebUI, BitTorrent, Downloads, VueTorrent, TagsAndCategories, Connection },
name: "Settings",
components: {
WebUI,
BitTorrent,
Downloads,
VueTorrent,
TagsAndCategories,
Connection,
},
mixins: [SettingsTab],
data() {
return {
tab: null,
items: [],
peers: [],
mdiClose, mdiContentSave
}
mdiClose,
mdiContentSave,
};
},
computed: {
...mapGetters(['getSettings']),
...mapGetters(["getSettings"]),
settings() {
return this.getSettings()
return this.getSettings();
},
isPhone() {
return this.$vuetify.breakpoint.xsOnly
}
return this.$vuetify.breakpoint.xsOnly;
},
},
mounted() {
this.$store.dispatch('FETCH_SETTINGS')
this.$store.dispatch("FETCH_SETTINGS");
},
methods: {
close() {
this.$router.back()
}
}
}
this.$router.back();
},
},
};
</script>