Added guides
This commit is contained in:
8
FAQ.md
8
FAQ.md
@@ -137,7 +137,7 @@ re-processed accordingly.
|
||||
To resolve this conflict and sync the backend with your local state:
|
||||
|
||||
* Go to the `WebUI > Backends`.
|
||||
* Under the relevant backend, find the **Frequently used commands** list.
|
||||
* Under the relevant backend, find the **Quick operations** list.
|
||||
* Select **3. Force export local play state to this backend.**
|
||||
|
||||
This operation will overwrite the backend's watch state with your current local state to bring them back in sync.
|
||||
@@ -179,7 +179,7 @@ To synchronize both backends correctly:
|
||||
* Enable **Full Import** for that backend.
|
||||
* Go to `Tasks` page, and run the **Import** task via `Run via console` button.
|
||||
* Once the import is complete, **add the second backend** (the one with incorrect or outdated play state).
|
||||
* Under the newly added backend, locate the **Frequently used commands** section.
|
||||
* Under the newly added backend, locate the **Quick operations** section.
|
||||
* Select **3. Force export local play state to this backend.**
|
||||
|
||||
This will push your local watch state to the backend and ensure both are in sync.
|
||||
@@ -195,7 +195,7 @@ local one, the export may be skipped.
|
||||
To confirm if this is the issue, follow these steps:
|
||||
|
||||
1. Go to the `WebUI > Backends`.
|
||||
2. Under the relevant backend, locate the **Frequently Used Commands** section.
|
||||
2. Under the relevant backend, locate the **Quick operations** section.
|
||||
3. Select **7. Run export and save debug log.**
|
||||
|
||||
This will generate a log file at `/config/user@backend_name.export.txt` If the file is too large to view in a regular
|
||||
@@ -218,7 +218,7 @@ If you see messages such as `Backend date is equal or newer than database date.`
|
||||
To override the date check and force an update do the following:
|
||||
|
||||
* Go to the `WebUI > Backends`.
|
||||
* Under the relevant backend, find the **Frequently used commands** list.
|
||||
* Under the relevant backend, find the **Quick operations** list.
|
||||
* Select **3. Force export local play state to this backend.**
|
||||
|
||||
This will sync your local database state to the backend, ignoring date comparisons.
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vendor/bin/phpunit",
|
||||
"tests": "vendor/bin/phpunit"
|
||||
"tests": "vendor/bin/phpunit",
|
||||
"update_ui": "yarn --cwd ./frontend upgrade --latest",
|
||||
"generate": "yarn --cwd ./frontend run generate"
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<hr>
|
||||
<label class="label has-text-danger" for="backup_data">
|
||||
@@ -282,6 +283,22 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backends.length < 1">
|
||||
<hr>
|
||||
<label class="label" for="force_import">
|
||||
Force one time import from this backend?
|
||||
</label>
|
||||
<div class="control">
|
||||
<input id="force_import" type="checkbox" class="switch is-success" v-model="force_import">
|
||||
<label for="force_import">Yes</label>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info-circle"></i></span>
|
||||
Run a one time import from this backend after adding it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backends.length > 0">
|
||||
<hr>
|
||||
<label class="label has-text-danger" for="force_export">
|
||||
@@ -296,6 +313,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -331,7 +349,7 @@ import request from '~/utils/request'
|
||||
import {awaitElement, explode, notification, ucFirst} from '~/utils/index'
|
||||
import {useStorage} from "@vueuse/core";
|
||||
|
||||
const emit = defineEmits(['addBackend', 'backupData', 'forceExport'])
|
||||
const emit = defineEmits(['addBackend', 'backupData', 'forceExport', 'forceImport'])
|
||||
|
||||
const props = defineProps({
|
||||
backends: {
|
||||
@@ -375,6 +393,7 @@ const exposeToken = ref(false)
|
||||
const error = ref()
|
||||
const backup_data = ref(true)
|
||||
const force_export = ref(false)
|
||||
const force_import = ref(false)
|
||||
|
||||
const isLimited = ref(false)
|
||||
const accessTokenResponse = ref({})
|
||||
@@ -678,6 +697,10 @@ const addBackend = async () => {
|
||||
emit('forceExport', backend)
|
||||
}
|
||||
|
||||
if (true === Boolean(force_import?.value ?? false)) {
|
||||
emit('forceImport', backend)
|
||||
}
|
||||
|
||||
emit('addBackend')
|
||||
|
||||
return true
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!selectedRequest">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12">
|
||||
<h2 class="title is-4">What are you trying to do?</h2>
|
||||
</div>
|
||||
<div v-for="(choice, index) in choices" :key="index" class="column is-6-tablet is-12-mobile">
|
||||
<div class="box content">
|
||||
<h3 class="title is-5">{{ choice.title }}</h3>
|
||||
<p>{{ choice.text }}</p>
|
||||
<button class="button is-link is-outlined" @click="pickRequest(index)">
|
||||
<span class="icon"><i class="fas fa-cogs"/></span>
|
||||
<span>Start</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRequest">
|
||||
<div v-for="stepId in steps" :key="stepId" v-show="stepId === currentStep" class="box">
|
||||
<slot :name="stepId">
|
||||
<h3 class="title is-5">Step: {{ stepId }}</h3>
|
||||
<p>Missing a guide for <code>{{ stepId }}</code> step.</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-fullwidth">
|
||||
<button class="button is-fullwidth is-info" @click="nextStep" :disabled="isLastStep">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button ml-2" @click="reset" :class="{'is-danger': !isLastStep, 'is-success': isLastStep}">
|
||||
{{ !isLastStep ? 'Reset' : 'Completed' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
Step {{ currentStepIndex + 1 }} of {{ steps.length }}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<Message message_class="is-background-warning-90 has-text-dark" icon="fas fa-info-circle" title="Important">
|
||||
Your progress is saved even if you navigate away. So feel free to do the steps and come back after each one.
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
const choices = [
|
||||
{
|
||||
title: 'One-way sync',
|
||||
text: 'For example, You want to import data from plex, and send it to jellyfin/emby but not the other way around',
|
||||
steps: ['add_backend', 'import_data', 'force_export'],
|
||||
},
|
||||
{
|
||||
title: 'Two-way sync',
|
||||
text: 'This will allow all backends to sync with each other. I.e. plex to jellyfin, jellyfin to emby, emby to plex.',
|
||||
steps: ['add_backend', 'import_data', 'force_export', 'enabled_import'],
|
||||
},
|
||||
{
|
||||
title: 'Enable webhooks',
|
||||
text: 'Step step on how to get webhooks working.',
|
||||
steps: ['add_backend', 'import_data', 'force_export', 'enabled_import'],
|
||||
},
|
||||
{
|
||||
title: 'Enable Sub-users',
|
||||
text: 'Guide on how to enable sub-users.',
|
||||
steps: ['sub_users_main_user', 'sub_user_create'],
|
||||
},
|
||||
]
|
||||
|
||||
const selectedRequest = useStorage('guide-request', null)
|
||||
const currentStepIndex = useStorage('guide-step-index', 0)
|
||||
|
||||
const steps = computed(() => {
|
||||
const c = choices[selectedRequest.value] || null
|
||||
console.log(c)
|
||||
return c ? choices[selectedRequest.value].steps : []
|
||||
})
|
||||
|
||||
const currentStep = computed(() => steps.value[currentStepIndex.value])
|
||||
const isLastStep = computed(() => currentStepIndex.value >= steps.value.length - 1)
|
||||
|
||||
const pickRequest = request => {
|
||||
selectedRequest.value = String(request)
|
||||
currentStepIndex.value = 0
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (!isLastStep.value) {
|
||||
currentStepIndex.value++
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
selectedRequest.value = null
|
||||
currentStepIndex.value = 0
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +1,24 @@
|
||||
<template>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12">
|
||||
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
|
||||
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
|
||||
|
||||
<Message v-if="error" message_class="has-background-warning-90 has-text-dark" title="Error"
|
||||
icon="fas fa-exclamation" :message="error"/>
|
||||
|
||||
<div class="content" v-html="content" v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStorage} from '@vueuse/core'
|
||||
import {marked} from 'marked'
|
||||
import {baseUrl} from 'marked-base-url'
|
||||
import {disableOpacity, enableOpacity} from "~/utils/index.js";
|
||||
import Message from "~/components/Message.vue";
|
||||
|
||||
const props = defineProps({
|
||||
file: {
|
||||
@@ -17,38 +29,59 @@ const props = defineProps({
|
||||
|
||||
const content = ref('')
|
||||
const api_url = useStorage('api_url', '')
|
||||
const error = ref('')
|
||||
const isLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch(`${api_url.value}${props.file}?_=${Date.now()}`)
|
||||
const text = await response.text()
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await fetch(`${api_url.value}${props.file}?_=${Date.now()}`)
|
||||
if (!response.ok) {
|
||||
const err = await parse_api_response(response)
|
||||
console.log(err)
|
||||
error.value = err.error.message
|
||||
return
|
||||
}
|
||||
|
||||
marked.use({
|
||||
gfm: true,
|
||||
hooks: {
|
||||
postprocess: (text) => {
|
||||
// -- replace github [! with icon
|
||||
text = text.replace(/\[!IMPORTANT\]/g, `
|
||||
<span class="is-block title is-4">
|
||||
const text = await response.text()
|
||||
marked.use({
|
||||
gfm: true,
|
||||
hooks: {
|
||||
postprocess: (text) => {
|
||||
// -- replace GitHub [! with icon
|
||||
text = text.replace(/\[!IMPORTANT]/g, `
|
||||
<span class="is-block title mb-2 is-5">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger fa-fade"></i></span>
|
||||
<span>Important</span>
|
||||
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger"></i></span>
|
||||
<span>IMPORTANT</span>
|
||||
</span>
|
||||
</span>`)
|
||||
|
||||
text = text.replace(/\[!NOTE\]/g, `
|
||||
<span class="is-block title is-4">
|
||||
text = text.replace(/\[!NOTE]/g, `
|
||||
<span class="is-block title is-5">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info-circle has-text-info-50"></i></span>
|
||||
<span>Note</span>
|
||||
<span class="icon"><i class="fas fa-info has-text-info-50"></i></span>
|
||||
<span>NOTE</span>
|
||||
</span>
|
||||
</span>`)
|
||||
return text
|
||||
}
|
||||
},
|
||||
...baseUrl(api_url.value),
|
||||
});
|
||||
|
||||
content.value = marked.parse(text)
|
||||
text = text.replace(
|
||||
/<!--\s*?i:([\w.-]+)\s*?-->/g,
|
||||
(_, list) => `<span class="icon"><i class="fas ${list.split('.').map(n => n.trim()).join(' ')}"></i></span>`
|
||||
);
|
||||
|
||||
return text
|
||||
}
|
||||
},
|
||||
...baseUrl(api_url.value),
|
||||
});
|
||||
content.value = String(marked.parse(text))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
error.value = e.message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -152,11 +152,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end pr-3">
|
||||
<div class="navbar-item" v-if="hasAPISettings && !showConnection">
|
||||
<button class="button is-dark" @click="showUserSelection = !showUserSelection" v-tooltip="'Change User'">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="hasAPISettings && !showConnection">
|
||||
<div class="navbar-item">
|
||||
<NuxtLink class="button is-dark" v-tooltip="'Guides'" to="/help">
|
||||
<span class="icon"><i class="fas fa-circle-question"/></span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="hasAPISettings && !showConnection">
|
||||
<button class="button is-dark" @click="showUserSelection = !showUserSelection" v-tooltip="'Change User'">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark" @click="showConnection = !showConnection"
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-primary" v-tooltip.bottom="'Add New Backend'"
|
||||
@click="toggleForm = !toggleForm" :disabled="isLoading">
|
||||
<button class="button is-primary" @click="toggleForm = !toggleForm" :disabled="isLoading">
|
||||
<span class="icon"><i class="fas fa-add"/></span>
|
||||
<span>Add Backend</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
@@ -156,7 +156,7 @@
|
||||
<div class="control is-fullwidth has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="selectedCommand" @change="forwardCommand(backend)">
|
||||
<option value="" disabled>Frequently used commands</option>
|
||||
<option value="" disabled>Quick operations</option>
|
||||
<option v-for="(command, index) in usefulCommands" :key="`qc-${index}`" :value="index"
|
||||
:disabled="!check_state(backend, command)">
|
||||
{{ command.id }}. {{ command.title }}
|
||||
@@ -305,7 +305,7 @@ const handleEvents = async (event, backend) => {
|
||||
switch (event) {
|
||||
case 'backupData':
|
||||
try {
|
||||
const backup_status = await queue_event('run_console', {
|
||||
await queue_event('run_console', {
|
||||
command: 'state:backup',
|
||||
args: [
|
||||
'-v',
|
||||
@@ -317,8 +317,6 @@ const handleEvents = async (event, backend) => {
|
||||
'{user}.{backend}.{date}.initial_backup.json',
|
||||
]
|
||||
})
|
||||
console.log(backup_status);
|
||||
|
||||
notification('info', 'Info', `We are going to initiate a backup for '${backend.value.name}' in little bit.`, 5000)
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Failed to queue backup request. ${e.message}`)
|
||||
@@ -326,26 +324,42 @@ const handleEvents = async (event, backend) => {
|
||||
break
|
||||
case 'forceExport':
|
||||
try {
|
||||
const export_status = await queue_event('run_console', {
|
||||
await queue_event('run_console', {
|
||||
command: 'state:export',
|
||||
args: [
|
||||
'-fi',
|
||||
'-v',
|
||||
'--user',
|
||||
api_user.value,
|
||||
'--dry-run',
|
||||
'--select-backend',
|
||||
backend.value.name,
|
||||
]
|
||||
}, 180)
|
||||
|
||||
console.log(export_status);
|
||||
|
||||
notification('info', 'Info', `Soon we are going to force export the local data to '${backend.value.name}'.`, 5000)
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Failed to queue force export request. ${e.message}`)
|
||||
}
|
||||
break
|
||||
case 'forceImport':
|
||||
try {
|
||||
await queue_event('run_console', {
|
||||
command: 'state:import',
|
||||
args: [
|
||||
'-f',
|
||||
'-v',
|
||||
'--user',
|
||||
api_user.value,
|
||||
'--select-backend',
|
||||
backend.value.name,
|
||||
]
|
||||
}, 180)
|
||||
|
||||
notification('info', 'Info', `Soon we will import data from '${backend.value.name}'.`, 5000)
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Failed to queue force export request. ${e.message}`)
|
||||
}
|
||||
break
|
||||
case 'addBackend':
|
||||
toggleForm.value = false
|
||||
await loadContent()
|
||||
|
||||
@@ -44,11 +44,13 @@
|
||||
<p class="control" v-if="!isLoading">
|
||||
<button class="button is-primary" type="button" :disabled="hasPrefix" @click="RunCommand">
|
||||
<span class="icon"><i class="fa fa-paper-plane"></i></span>
|
||||
<span>Execute</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control" v-if="isLoading">
|
||||
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
|
||||
<span class="icon"><i class="fa fa-power-off"></i></span>
|
||||
<span>Close</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
<span class="icon"><i class="fas fa-cogs"/></span>
|
||||
Getting Started
|
||||
</span>
|
||||
<div class="is-pulled-right"></div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">A step by step guide to get you started with specific tasks.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12">
|
||||
<InteractionGuide>
|
||||
<template #add_backend>
|
||||
<div id="add_backend">
|
||||
<h3 class="title is-5">Step: Add Backend</h3>
|
||||
<p>
|
||||
1. ...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #import_data>
|
||||
<div id="import_data">
|
||||
<h3 class="title is-5">Step: Import Data</h3>
|
||||
<p>
|
||||
1. ...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #force_export>
|
||||
<div id="force_export">
|
||||
<h3 class="title is-5">Step: Force Export</h3>
|
||||
<p>
|
||||
1. ...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #enabled_import>
|
||||
<div id="enabled_import">
|
||||
<h3 class="title is-5">Step: Enable Import</h3>
|
||||
<p>
|
||||
1. ...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</InteractionGuide>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InteractionGuide from "~/components/InteractionGuide.vue"
|
||||
</script>
|
||||
12
frontend/pages/help/[...slug].vue
Normal file
12
frontend/pages/help/[...slug].vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12">
|
||||
<Markdown :file="`/guides/${slug}.md`"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const slug = ref(`${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
|
||||
</script>
|
||||
67
frontend/pages/help/index.vue
Normal file
67
frontend/pages/help/index.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
<span class="icon"><i class="fas fa-hands-helping"/></span>
|
||||
{{ 'Getting started' }}
|
||||
</span>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
This page contains guides to help you get started with WatchState. This is an early version, we are still
|
||||
working on the guides.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div v-for="choice in choices" :key="choice.url" class="column is-6-tablet is-12-mobile">
|
||||
<div class="box content" style="height: 100%">
|
||||
<h3 class="title is-5">
|
||||
<NuxtLink :to="choice.url" class="has-text-link" v-text="`${choice.number}. ${choice.title}`"
|
||||
v-if="choice.url"/>
|
||||
<span v-else>{{ `${choice.number}. ${choice.title}` }}</span>
|
||||
</h3>
|
||||
<hr>
|
||||
<Message message_class="has-background-warning-90 has-text-dark" v-if="!choice.url" class="p-1">
|
||||
<p>
|
||||
<span class="icon"><i class="fas fa-exclamation has-text-danger"/></span>
|
||||
<span>This guide is not available yet.</span>
|
||||
</p>
|
||||
</Message>
|
||||
<p>{{ choice.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const choices = [
|
||||
{
|
||||
number: 1,
|
||||
title: 'One-way sync',
|
||||
text: 'For example, You want to import data from plex, and send it to jellyfin/emby but not the other way around.',
|
||||
url: '/help/one-way-sync',
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Two-way sync',
|
||||
text: 'This will allow all backends to sync with each other. I.e. plex to jellyfin, jellyfin to emby, emby to plex.',
|
||||
url: null
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Enable webhooks',
|
||||
text: 'How to enable webhooks for your backends and for sub-users.',
|
||||
url: null
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: 'Enable Sub-users',
|
||||
text: 'Guide on how to enable sub-users.',
|
||||
url: null
|
||||
},
|
||||
]
|
||||
</script>
|
||||
@@ -105,22 +105,33 @@
|
||||
<div class="column is-12">
|
||||
<div class="content">
|
||||
<Message title="Welcome" message_class="has-background-info-90 has-text-dark" icon="fas fa-heart">
|
||||
If you have question, or want clarification on something, or just want to chat with other users,
|
||||
you are
|
||||
welcome to join our <span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
|
||||
<span>
|
||||
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank"
|
||||
v-text="'Discord server'"/>
|
||||
</span>
|
||||
</span>. For bug reports, feature requests, or contributions, please visit the
|
||||
<span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
|
||||
<p>
|
||||
If you have question, or want clarification on something, or just want to chat with other users,
|
||||
you are
|
||||
welcome to join our <span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
|
||||
<span>
|
||||
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose"
|
||||
target="_blank" v-text="'GitHub repository'"/>
|
||||
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank"
|
||||
v-text="'Discord server'"/>
|
||||
</span>
|
||||
</span>.
|
||||
</span>. For bug reports, feature requests, or contributions, please visit the
|
||||
<span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
|
||||
<span>
|
||||
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose"
|
||||
target="_blank" v-text="'GitHub repository'"/>
|
||||
</span>
|
||||
</span>.
|
||||
</p>
|
||||
<p>
|
||||
We have recently added a guides page to help you get started with WatchState. You can find it
|
||||
<span class="icon-text is-underlined">
|
||||
<span class="icon"><i class="fas fa-question-circle"/></span>
|
||||
<span>
|
||||
<NuxtLink to="/help" v-text="'here'"/>
|
||||
</span>
|
||||
</span>, it still very early version and only contains a few guides, but we are working on it.
|
||||
</p>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6335,9 +6335,9 @@ write-file-atomic@^6.0.0:
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
ws@^8.18.1:
|
||||
version "8.18.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
|
||||
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
|
||||
version "8.18.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
|
||||
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
|
||||
119
guides/one-way-sync.md
Normal file
119
guides/one-way-sync.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# One-way sync
|
||||
|
||||
One-way sync in WatchState is the ability to sync data from one backend to one or more backends without
|
||||
effecting the data in the source backend. This is useful for scenarios where you want to keep a backup of your data or
|
||||
sync data to a different environment without modifying the original data.
|
||||
|
||||
# Use cases
|
||||
|
||||
- you want to try jellyfin/plex/emby without altering your original data.
|
||||
- you want to keep your jellyfin/emby/plex backend in sync with your preferred media backend.
|
||||
- You made new media backend that doesn't have your play state yet, and you want to get it in sync with your main
|
||||
backend.
|
||||
|
||||
# How to set up one-way sync
|
||||
|
||||
First, go to <!--i:fa-server--> Backends and click on <!-- i:fa-plus --> plus button. follow the interactive setup
|
||||
guide, when you reach the step with `Export data to this backend?`, select `No`, this instruction applies to the main
|
||||
backend only. as you don't want to alter its data. Keep `Import data from this backend?` to `Yes`, this will allow you
|
||||
to import data from the backend.
|
||||
|
||||
> [!NOTE]
|
||||
> It's recommended to keep `Create backup for this backend data?` to `Yes`, this will create a snapshot of your
|
||||
> backend data, so that you may return to it should something happens. via <!--i:fa-tools--> Tools > <!--i:fa-sd-card-->
|
||||
> Backups.
|
||||
|
||||
|
||||
Enable `Force one time import from this backend?` option to get your current data into WatchState. This will import all
|
||||
the data from the backend into WatchState.
|
||||
|
||||
# Importing the data
|
||||
|
||||
It will take a while to import the data, depending on the size of your library.
|
||||
|
||||
To see the import status, you can go to <!--i:fa-globe--> Logs page, and check the `task.XXXXXXX.log` file. Or
|
||||
via <!--i:fa-ellipsis-vertical--> More > <!--i:fa-calendar-alt--> Events page, and look for `run_console` event. We
|
||||
recommend the tasks log as it's updated more frequently.
|
||||
|
||||
To know if it has finished, you can look for the following message:
|
||||
|
||||
```text
|
||||
[DD/MM HH:mm:SS]: NOTICE: SYSTEM: Completed 'XXX' requests in 'XXX.XXX's for 'main' backends.
|
||||
|
||||
┌─────────┬───────┬─────────┬────────┐
|
||||
│ Type │ Added │ Updated │ Failed │
|
||||
├─────────┼───────┼─────────┼────────┤
|
||||
│ Movie │ XXX │ XXX │ XXX │
|
||||
├─────────┼───────┼─────────┼────────┤
|
||||
│ Episode │ XXX │ XXX │ XXX │
|
||||
└─────────┴───────┴─────────┴────────┘
|
||||
```
|
||||
|
||||
Or go to the <!--i:fa-server--> Backends page, and check the backend last import date, if it shows the current time it
|
||||
means the import is done.
|
||||
|
||||
After you see the message, you can go to <!--i:fa-history--> History page and see the import history. Navigate around to
|
||||
check the data, once you are satisfied with the data, proceed to the next step.
|
||||
|
||||
# Adding the other backends
|
||||
|
||||
For each backend do these steps
|
||||
|
||||
## Step 1
|
||||
|
||||
Do exactly as you did for the main backend, But change the follow options:
|
||||
|
||||
- `Import data from this backend?`: No
|
||||
- `Import metadata only from this backend?`: Yes
|
||||
- `Export data to this backend?`: Yes
|
||||
- `Force Export local data to this backend?`: depends
|
||||
- If you only have 1 extra backend and already have imported your main backend data, then select `Yes`, and skip
|
||||
step 2.
|
||||
- Otherwise, keep it disabled and proceed to step 2.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> It's really important that you select those options, otherwise you might inadvertently alter the data in the main
|
||||
> backend.
|
||||
>
|
||||
> The option `Import metadata only from this backend?` only shows up when you select `No` for
|
||||
`Import data from this backend?`.
|
||||
|
||||
## Step 2
|
||||
|
||||
You have two options,
|
||||
|
||||
### Option 1 (1 Extra backend)
|
||||
|
||||
Go to <!--i:fa-server--> Backends page beneath the backend there is `Quick operations` list, select
|
||||
`2. Force export local play state to this backend.`. Once you select the option, you will be redirected to
|
||||
the <!--i:fa-ellipsis-vertical--> More > <!--i:fa-terminal--> Console page. Once you are there, the command will be
|
||||
pre-filled for you, just hit enter to run it. Or click on <!--i:fa-terminal--> Execute button.
|
||||
|
||||
### Option 2 (Multiple backends)
|
||||
|
||||
If you have multiple backends that you want to sync, then skip step 2 for now, and add all your backends.
|
||||
|
||||
Once you are done, go to <!--i:fa-tools--> Tools > <!--i:fa-terminal--> Console page, and run the following command:
|
||||
|
||||
```bash
|
||||
state:export -fi -v -u main
|
||||
```
|
||||
|
||||
This command will force export your locally stored play state to all export enabled backends. Once that is done proceed
|
||||
to the next step.
|
||||
|
||||
## Enable Automation
|
||||
|
||||
If you are satisfied with the results, and you want to automate the process from now on, go to <!--i:fa-tasks--> Tasks
|
||||
page. There are two tasks that you need to enable by clicking on the slider next to the name `Import` and `Export`.
|
||||
|
||||
To change how often the tasks runs, you have to go to <!--i:fa-cogs--> Env page, click on <!--i:fa-plus--> add button,
|
||||
select the relevant environment variable. In this case, `WS_CRON_EXPORT_AT` and `WS_CRON_IMPORT_AT`. These two variables
|
||||
accept valid CRON timer expressions. if you want to run the export task every 6 hours for example, you can set the
|
||||
variable `WS_CRON_EXPORT_AT` value to `0 */6 * * *`. For more information about CRON expressions, check
|
||||
out [crontab.guru](https://crontab.guru/).
|
||||
|
||||
## Enabling webhooks
|
||||
|
||||
To know how to enable webhooks for faster sync operations, Please check out the webhooks guide.
|
||||
|
||||
@@ -43,6 +43,7 @@ final class ServeStatic implements LoggerAwareInterface
|
||||
|
||||
private const array MD_IMAGES = [
|
||||
'/screenshots' => __DIR__ . '/../../',
|
||||
'/guides' => __DIR__ . '/../../',
|
||||
];
|
||||
|
||||
public function __construct(private string|null $staticPath = null)
|
||||
|
||||
Reference in New Issue
Block a user