Massive API & WebUI changes.
This commit is contained in:
35
FAQ.md
35
FAQ.md
@@ -341,27 +341,13 @@ $ docker exec -ti watchstate console system:tasks
|
||||
|
||||
### How to add webhooks?
|
||||
|
||||
The Webhook URL is backend specific, the request path is `/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`,
|
||||
Where `[BACKEND_NAME]` is the name of the backend you want to add webhook for, and `[APIKEY]` is the global api key
|
||||
which you can get via the `system:apikey` command. Typically, the full path
|
||||
is `http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`. if the tool
|
||||
port is directly exposed or via the reverse proxy you have setup.
|
||||
|
||||
If your media backend support sending headers then remove query parameter `?apikey=[APIKEY]`, and add this header
|
||||
|
||||
```
|
||||
Authorization: Bearer [APIKEY]
|
||||
```
|
||||
|
||||
To see your global api key run the following command:
|
||||
|
||||
```bash
|
||||
$ docker exec -ti watchstate console system:apikey
|
||||
```
|
||||
The Webhook URL is backend specific, the request path is `/v1/api/backend/[BACKEND_NAME]/webhook`,
|
||||
Where `[BACKEND_NAME]` is the name of the backend you want to add webhook for. Typically, the full URL
|
||||
is `http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook]`.
|
||||
|
||||
> [!NOTE]
|
||||
> You will keep seeing the `webhook.token` key, it's being kept for backward compatibility, and will be removed in the
|
||||
> future.
|
||||
> future. It has no effect except as pointer to the new method.
|
||||
|
||||
-----
|
||||
|
||||
@@ -371,10 +357,9 @@ Go to your Manage Emby Server > Server > Webhooks > (Click Add Webhook)
|
||||
|
||||
##### Webhook/Notifications URL:
|
||||
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`
|
||||
|
||||
* Replace `[BACKEND_NAME]` with the name you have chosen for your backend.
|
||||
* Replace `[APIKEY]` with the global apikey.
|
||||
|
||||
##### Request content type (Emby v4.9+):
|
||||
|
||||
@@ -412,10 +397,9 @@ Go to your Plex Web UI > Settings > Your Account > Webhooks > (Click ADD WEBHOOK
|
||||
|
||||
##### URL:
|
||||
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook?apikey=[APIKEY]`
|
||||
`http://localhost:8080/v1/api/backend/[BACKEND_NAME]/webhook`
|
||||
|
||||
* Replace `[BACKEND_NAME]` with the name you have chosen for your backend.
|
||||
* Replace `[APIKEY]` with the global apikey.
|
||||
|
||||
> [!NOTE]
|
||||
> If you use multiple plex servers and use the same PlexPass account for all of them, You have to add each backend
|
||||
@@ -465,13 +449,6 @@ go back again to dashboard > plugins > webhook. Add `Add Generic Destination`,
|
||||
|
||||
Toggle this checkbox.
|
||||
|
||||
### Add Request Header
|
||||
|
||||
* Key: `Authorization`
|
||||
* Value: `Bearer [APIKEY]`
|
||||
|
||||
Replace `[APIKEY]` with the global apikey.
|
||||
|
||||
Click `Save`
|
||||
|
||||
---
|
||||
|
||||
@@ -9,6 +9,11 @@ out of the box, this tool support `Jellyfin`, `Plex` and `Emby` media servers.
|
||||
|
||||
## updates
|
||||
|
||||
### 2024-05-04
|
||||
|
||||
The new webhook endpoint no longer requires a key, and it's now open to public you just need to specify the backend
|
||||
name.
|
||||
|
||||
### 2024-04-30 - [BREAKING CHANGE]
|
||||
|
||||
We are going to retire the old webhooks endpoint, please refer to the [FAQ](FAQ.md#how-to-add-webhooks) to know how to
|
||||
|
||||
@@ -73,6 +73,8 @@ return (function () {
|
||||
|
||||
$dbFile = ag($config, 'path') . '/db/watchstate_' . ag($config, 'database.version') . '.db';
|
||||
|
||||
$config['api']['logfile'] = ag($config, 'tmpDir') . '/logs/access.' . $logDateFormat . '.log';
|
||||
|
||||
$config['database'] += [
|
||||
'file' => $dbFile,
|
||||
'dsn' => 'sqlite:' . $dbFile,
|
||||
@@ -91,7 +93,7 @@ return (function () {
|
||||
];
|
||||
|
||||
$config['webhook'] = [
|
||||
'logfile' => ag($config, 'tmpDir') . '/logs/access.' . $logDateFormat . '.log',
|
||||
'logfile' => ag($config, 'tmpDir') . '/logs/webhook.' . $logDateFormat . '.log',
|
||||
'dumpRequest' => (bool)env('WS_WEBHOOK_DUMP_REQUEST', false),
|
||||
'tokenLength' => (int)env('WS_WEBHOOK_TOKEN_LENGTH', 16),
|
||||
'file_format' => (string)env('WS_WEBHOOK_LOG_FILE_FORMAT', 'webhook.{backend}.{event}.{id}.json'),
|
||||
|
||||
330
frontend/pages/backend/[backend]/edit.vue
Normal file
330
frontend/pages/backend/[backend]/edit.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-clearfix">
|
||||
<span class="title is-4">
|
||||
<NuxtLink href="/backends">Backends</NuxtLink>
|
||||
- Edit:
|
||||
<NuxtLink :href="'/backend/' + id">{{ id }}</NuxtLink>
|
||||
</span>
|
||||
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="isLoading">
|
||||
<Message message_class="is-info" title="Information">
|
||||
<span class="icon"><i class="fas fa-spinner fa-pulse"></i></span>
|
||||
<span>Loading backend settings, please wait...</span>
|
||||
</Message>
|
||||
</div>
|
||||
<div v-else class="column is-12">
|
||||
<form id="backend_edit_form" @submit.prevent="saveContent">
|
||||
<div class="box">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required readonly disabled>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
|
||||
case a-z, 0-9 and _</code> only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.type" readonly disabled>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend URL</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.url" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Enter the URL of the backend.
|
||||
<a v-if="'plex' === backend.type" href="javascript:void(0)">Get associated servers with token. NYI</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Backend API token
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
<template v-if="'plex'===backend.type">
|
||||
Enter the <code>X-Plex-Token</code>. <a target="_blank"
|
||||
href="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/">
|
||||
Visit This article for more information
|
||||
</a>.
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate a new API token from <code>Dashboard > Settings > API Keys</code>.
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Unique ID</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.uuid" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-server" v-if="!uuidLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
The Unique identifier for the backend.
|
||||
<a href="javascript:void(0)" @click="getUUid">Get from the backend.</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend User ID</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="backend.user" class="is-capitalized">
|
||||
<option v-for="user in users" :key="'uid-'+user.id" :value="user.id">
|
||||
<template v-if="'plex' === backend">
|
||||
{{ user.id }} - {{ user.name }}
|
||||
</template>
|
||||
<template v-else>{{ user.name }}</template>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user-tie" v-if="!usersLoading"></i>
|
||||
<i class="fas fa-spinner fa-pulse" v-else></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
The user ID of the backend.
|
||||
<a href="javascript:void(0)" @click="getUsers">
|
||||
Retrieve User ids from backend.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backend.import">
|
||||
<label class="label" for="backend_import">Import data from this backend</label>
|
||||
<div class="control">
|
||||
<input id="backend_import" type="checkbox" class="switch is-success" v-model="backend.import.enabled">
|
||||
<label for="backend_import">Enable</label>
|
||||
<p class="help">
|
||||
Import means to get the data from the backend and store it in the database.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backend.export">
|
||||
<label class="label" for="backend_export">Export data to this backend</label>
|
||||
<div class="control">
|
||||
<input id="backend_export" type="checkbox" class="switch is-success" v-model="backend.export.enabled">
|
||||
<label for="backend_export">Enable</label>
|
||||
<p class="help">
|
||||
Export means to send the data from the database to this backend.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backend.webhook">
|
||||
<label class="label" for="webhook_match_user">Webhook match user</label>
|
||||
<div class="control">
|
||||
<input id="webhook_match_user" type="checkbox" class="switch is-success"
|
||||
v-model="backend.webhook.match.user">
|
||||
<label for="webhook_match_user">Enable</label>
|
||||
<p class="help">
|
||||
Check webhook payload for user id match. if it does not match, the payload will be ignored.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="backend.webhook">
|
||||
<label class="label" for="webhook_match_uuid">Webhook match backend id</label>
|
||||
<div class="control">
|
||||
<input id="webhook_match_uuid" type="checkbox" class="switch is-success"
|
||||
v-model="backend.webhook.match.uuid">
|
||||
<label for="webhook_match_uuid">Enable</label>
|
||||
<p class="help">
|
||||
Check webhook payload for backend unique id. if it does not match, the payload will be ignored.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" @click="showOptions = !showOptions">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i v-if="showOptions" class="fas fa-arrow-up"></i>
|
||||
<i v-else class="fas fa-arrow-down"></i>
|
||||
</span>
|
||||
<span>Optional options</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="columns is-multiline is-mobile" v-if="showOptions && backend.options">
|
||||
<template v-for="(val, key) in backend.options" :key="'bo-'+key">
|
||||
<div class="column is-5">
|
||||
<input type="text" class="input" :value="key" readonly disabled>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<input type="text" class="input" v-model="backend.options[key]">
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<button class="button is-danger" @click.prevent="removeOption(key)">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field has-text-right">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-save"></i>
|
||||
</span>
|
||||
<span>Save Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import 'assets/css/bulma-switch.css'
|
||||
|
||||
const id = useRoute().params.backend
|
||||
const backend = ref({})
|
||||
const showOptions = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const users = ref([])
|
||||
const usersLoading = ref(false)
|
||||
const uuidLoading = ref(false)
|
||||
|
||||
useHead({title: 'Backends - Edit: ' + id})
|
||||
|
||||
const loadContent = async () => {
|
||||
const content = await request(`/backend/${id}`)
|
||||
backend.value = await content.json()
|
||||
|
||||
await getUsers()
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const saveContent = async () => {
|
||||
const response = await request(`/backend/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(backend.value)
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
if (!response.ok) {
|
||||
alert('Failed to save backend settings.')
|
||||
}
|
||||
}
|
||||
|
||||
const removeOption = async (key) => {
|
||||
if (!confirm(`Are you sure you want to remove this option [${key}]?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
delete backend.value.options[key]
|
||||
|
||||
const response = await request(`/backend/${id}/option/${key}`, {method: 'DELETE'})
|
||||
}
|
||||
|
||||
const getUUid = async () => {
|
||||
const required_values = ['type', 'token', 'url'];
|
||||
|
||||
if (required_values.some(v => !backend.value[v])) {
|
||||
alert('Please fill all the required fields.')
|
||||
return
|
||||
}
|
||||
|
||||
uuidLoading.value = true
|
||||
|
||||
const response = await request(`/backends/uuid/${backend.value.type}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
token: backend.value.token,
|
||||
url: backend.value.url
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
uuidLoading.value = false
|
||||
|
||||
if (!response.ok) {
|
||||
alert('Failed to get the UUID from the backend.')
|
||||
return
|
||||
}
|
||||
|
||||
backend.value.uuid = json.identifier
|
||||
}
|
||||
|
||||
const getUsers = async (showAlert = true) => {
|
||||
const required_values = ['type', 'token', 'url', 'uuid'];
|
||||
|
||||
usersLoading.value = true
|
||||
|
||||
if (required_values.some(v => !backend.value[v])) {
|
||||
if (showAlert) {
|
||||
alert('Please fill all the required fields.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const response = await request(`/backends/users/${backend.value.type}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
token: backend.value.token,
|
||||
url: backend.value.url,
|
||||
uuid: backend.value.uuid,
|
||||
})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
|
||||
usersLoading.value = false
|
||||
|
||||
if (200 !== response.status) {
|
||||
alert(`${json.error.code}: ${json.error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
users.value = json
|
||||
}
|
||||
|
||||
onMounted(() => loadContent())
|
||||
|
||||
</script>
|
||||
@@ -14,6 +14,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const backend = useRoute().params.backend
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="title is-4">
|
||||
<NuxtLink href="/backends">Backends</NuxtLink>
|
||||
-
|
||||
<NuxtLink :href="'/backends/' + backend">{{ backend }}</NuxtLink>
|
||||
<NuxtLink :href="'/backend/' + backend">{{ backend }}</NuxtLink>
|
||||
: Info
|
||||
</span>
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-clearfix">
|
||||
<span class="title is-4">
|
||||
<NuxtLink href="/backends">Backends</NuxtLink>
|
||||
: Edit -
|
||||
<NuxtLink :href="'/backends/' + id">{{ id }}</NuxtLink>
|
||||
</span>
|
||||
|
||||
<div class="is-pulled-right">
|
||||
<div class="field is-grouped"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<form id="backend_edit_form" @submit.prevent="saveContent">
|
||||
<div class="box">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.name" required readonly disabled>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Choose a unique name for this backend. You cannot change it later. Backend name must be in <code>lower
|
||||
case a-z, 0-9 and _</code> only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth" disabled>
|
||||
<select v-model="backend.type" disabled class="is-capitalized">
|
||||
<option v-for="(bType, index) in supported" :key="'btype-'+index" :value="bType">
|
||||
{{ bType }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Select the correct backend type.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend URL</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.url" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Enter the URL of the backend.
|
||||
<a v-if="'plex' === backend.type" href="javascript:void(0)">Get associated servers with token.</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Token</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.token" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
Enter the token of the backend.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend User ID</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.user" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
The user ID of the backend. <a href="javascript:void(0)">Pull User ids from backend.</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Backend Unique ID</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="backend.uuid" required>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
<p class="help">
|
||||
The Unique identifier for the backend.
|
||||
<a href="javascript:void(0)">Pull from the backend</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const id = useRoute().params.backend
|
||||
const backend = ref({})
|
||||
const supported = ref([])
|
||||
|
||||
const loadContent = async () => {
|
||||
let content = await request('/system/supported')
|
||||
let json = await content.json()
|
||||
supported.value = json.supported
|
||||
|
||||
content = await request(`/backend/${id}`)
|
||||
json = await content.json()
|
||||
backend.value = json.backend
|
||||
}
|
||||
|
||||
onMounted(() => loadContent())
|
||||
|
||||
</script>
|
||||
@@ -27,11 +27,16 @@
|
||||
<div v-for="backend in backends" :key="backend.name" class="column is-6-tablet is-12-mobile">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-centered is-word-break">
|
||||
<NuxtLink :href="'/backends/' + backend.name">
|
||||
<p class="card-header-title is-centered is-word-break">
|
||||
<NuxtLink :href="'/backend/' + backend.name">
|
||||
{{ backend.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</p>
|
||||
<span class="card-header-icon" v-tooltip="'Edit Backend settings'">
|
||||
<NuxtLink :href="`/backend/${backend.name}/edit`">
|
||||
<span class="icon"><i class="fas fa-cog"></i></span>
|
||||
</NuxtLink>
|
||||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline is-mobile has-text-centered">
|
||||
@@ -82,8 +87,7 @@ const backends = ref([])
|
||||
const loadContent = async () => {
|
||||
backends.value = []
|
||||
const response = await request('/backends')
|
||||
const json = await response.json();
|
||||
backends.value = json.backends
|
||||
backends.value = await response.json()
|
||||
}
|
||||
|
||||
onMounted(() => loadContent())
|
||||
@@ -97,8 +101,7 @@ const updateValue = async (backend, key, newValue) => {
|
||||
}])
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
backends.value[backends.value.findIndex(b => b.name === backend.name)] = json.backend
|
||||
backends.value[backends.value.findIndex(b => b.name === backend.name)] = await response.json();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -60,9 +60,9 @@
|
||||
<table class="table is-fullwidth is-bordered is-striped is-hoverable has-text-centered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="25%">Key</th>
|
||||
<th style="width: 25%;">Key</th>
|
||||
<th>Value</th>
|
||||
<th width="10%">Actions</th>
|
||||
<th style="width: 10%;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{{ moment(history.updated).fromNow() }}
|
||||
</div>
|
||||
<div class="column is-6-mobile">
|
||||
<NuxtLink :href="'/backends/'+history.via">
|
||||
<NuxtLink :href="'/backend/'+history.via">
|
||||
{{ history.via }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<span class="icon" v-if="'access' === item.type"><i class="fas fa-key"></i></span>
|
||||
<span class="icon" v-if="'task' === item.type"><i class="fas fa-tasks"></i></span>
|
||||
<span class="icon" v-if="'app' === item.type"><i class="fas fa-bugs"></i></span>
|
||||
<span class="icon" v-if="'webhook' === item.type"><i class="fas fa-book"></i></span>
|
||||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
@@ -63,8 +64,8 @@ const logs = ref([])
|
||||
const loadContent = async () => {
|
||||
logs.value = []
|
||||
const response = await request('/logs')
|
||||
const json = await response.json();
|
||||
let data = json.logs;
|
||||
let data = await response.json();
|
||||
|
||||
data.sort((a, b) => new Date(b.modified) - new Date(a.modified));
|
||||
|
||||
logs.value = data;
|
||||
|
||||
@@ -83,10 +83,10 @@
|
||||
|
||||
<script setup>
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import moment from "moment";
|
||||
import request from "~/utils/request.js";
|
||||
import moment from 'moment'
|
||||
import request from '~/utils/request.js'
|
||||
|
||||
useHead({title: 'tasks'})
|
||||
useHead({title: 'Tasks'})
|
||||
|
||||
const tasks = ref([])
|
||||
const queued = ref([])
|
||||
@@ -96,7 +96,7 @@ const loadContent = async (clear = false) => {
|
||||
tasks.value = []
|
||||
}
|
||||
const response = await request('/tasks')
|
||||
const json = await response.json();
|
||||
const json = await response.json()
|
||||
tasks.value = json.tasks
|
||||
queued.value = json.queued
|
||||
}
|
||||
@@ -111,9 +111,7 @@ const toggleTask = async (task) => {
|
||||
});
|
||||
|
||||
const response = await request(`/tasks/${task.name}`)
|
||||
const json = await response.json();
|
||||
|
||||
tasks.value[tasks.value.findIndex(b => b.name === task.name)] = json.task
|
||||
tasks.value[tasks.value.findIndex(b => b.name === task.name)] = await response.json()
|
||||
}
|
||||
|
||||
const queueTask = async (task) => {
|
||||
@@ -123,8 +121,7 @@ const queueTask = async (task) => {
|
||||
|
||||
const response = await request(`/tasks/${task.name}/queue`, {method: 'POST'})
|
||||
if (response.ok) {
|
||||
await loadContent();
|
||||
await loadContent()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -2,47 +2,43 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Plex;
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Libs\Attributes\Route\Post;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
use Throwable;
|
||||
|
||||
final class GenerateAccessToken
|
||||
final class AccessToken
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
public const string URL = '%{api.prefix}/plex/accesstoken';
|
||||
|
||||
public function __construct(private iHttp $http)
|
||||
public function __construct(private readonly iHttp $http)
|
||||
{
|
||||
}
|
||||
|
||||
#[Post(self::URL . '/{id:backend}[/]', name: 'plex.accesstoken')]
|
||||
public function gAccesstoken(iRequest $request, array $args = []): iResponse
|
||||
#[Post(Index::URL . '/{name:backend}/accesstoken[/]', name: 'backend.accesstoken')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
$backend = ag($args, 'id');
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$data = DataUtil::fromArray($request->getParsedBody());
|
||||
|
||||
if (null === ($uuid = $data->get('uuid'))) {
|
||||
return api_error('No User (uuid) was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient($backend);
|
||||
} catch (RuntimeException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
if (null === ($id = $data->get('id'))) {
|
||||
return api_error('No id was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
$token = $client->getUserToken(
|
||||
userId: $uuid,
|
||||
userId: $id,
|
||||
username: $data->get('username', $client->getContext()->backendName . '_user'),
|
||||
);
|
||||
|
||||
@@ -59,7 +55,9 @@ final class GenerateAccessToken
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $arr);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
49
src/API/Backend/Discover.php
Normal file
49
src/API/Backend/Discover.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Backends\Plex\PlexClient;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
use Throwable;
|
||||
|
||||
final class Discover
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
public function __construct(private readonly iHttp $http)
|
||||
{
|
||||
}
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/discover[/]', name: 'backend.discover')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
|
||||
if (PlexClient::CLIENT_NAME !== $client->getType()) {
|
||||
return api_error('Discover is only available for Plex backends.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
assert($client instanceof PlexClient);
|
||||
|
||||
$list = $client::discover($this->http, $client->getContext()->backendToken);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ag($list, 'list', []));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,11 @@ final class Ignore
|
||||
$this->file = new ConfigFile(Config::get('path') . '/config/ignore.yaml', type: 'yaml', autoCreate: true);
|
||||
}
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds')]
|
||||
#[Get(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds')]
|
||||
public function ignoredIds(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = [];
|
||||
@@ -61,22 +61,23 @@ final class Ignore
|
||||
'created' => makeDate($date),
|
||||
];
|
||||
}
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$response = [
|
||||
'ignore' => $list,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $list);
|
||||
}
|
||||
|
||||
#[Delete(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds.delete')]
|
||||
#[Delete(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.delete')]
|
||||
public function deleteRule(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$data = $this->getBackends(name: $name);
|
||||
|
||||
if (empty($data)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$params = DataUtil::fromRequest($request);
|
||||
|
||||
if (null === ($rule = $params->get('rule'))) {
|
||||
@@ -98,11 +99,11 @@ final class Ignore
|
||||
return api_response(HTTP_STATUS::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Post(Index::URL . '/{name:backend}/ignore[/]', name: 'backends.backend.ignoredIds.add')]
|
||||
#[Post(Index::URL . '/{name:backend}/ignore[/]', name: 'backend.ignoredIds.add')]
|
||||
public function addRule(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$data = $this->getBackends(name: $name);
|
||||
|
||||
@@ -16,11 +16,11 @@ final class Index
|
||||
|
||||
public const string URL = '%{api.prefix}/backend';
|
||||
|
||||
#[Get(self::URL . '/{name:backend}[/]', name: 'backends.view')]
|
||||
#[Get(self::URL . '/{name:backend}[/]', name: 'backend.view')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$data = $this->getBackends(name: $name);
|
||||
@@ -29,17 +29,8 @@ final class Index
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
$data = array_pop($data);
|
||||
|
||||
$response = [
|
||||
...$data,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['backend' => $response]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ final class Info
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/info[/]', name: 'backends.backend.info')]
|
||||
public function backendsView(iRequest $request, array $args = []): iResponse
|
||||
#[Get(Index::URL . '/{name:backend}/info[/]', name: 'backend.info')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -40,20 +40,9 @@ final class Info
|
||||
|
||||
try {
|
||||
$data = $client->getInfo($opts);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $data);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$response = [
|
||||
'data' => $data,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,51 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend\Library;
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\API\Backend\Index as BackendsIndex;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Attributes\Route\Route;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Throwable;
|
||||
|
||||
final class Ignore
|
||||
final class Library
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Route(['POST', 'DELETE'], BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backends.library.ignore')]
|
||||
public function _invoke(iRequest $request, array $args = []): iResponse
|
||||
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backend.library')]
|
||||
public function listLibraries(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($id = DataUtil::fromRequest($request, true)->get('id', null))) {
|
||||
return api_error('No library id was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $client->listLibraries());
|
||||
} catch (RuntimeException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route(['POST', 'DELETE'], BackendsIndex::URL . '/{name:backend}/library/{id}[/]', name: 'backend.library.ignore')]
|
||||
public function ignoreLibrary(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($id = ag($args, 'id'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$remove = 'DELETE' === $request->getMethod();
|
||||
@@ -75,13 +94,6 @@ final class Ignore
|
||||
|
||||
$config->set("{$name}.options." . Options::IGNORE, implode(',', array_values($ignoreIds)))->persist();
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'type' => $config->get("{$name}.type"),
|
||||
'libraries' => $libraries,
|
||||
'links' => [
|
||||
'self' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}/library"),
|
||||
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
|
||||
],
|
||||
]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $libraries);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend\Library;
|
||||
|
||||
use App\API\Backend\Index as BackendsIndex;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class Index
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(BackendsIndex::URL . '/{name:backend}/library[/]', name: 'backends.library.list')]
|
||||
public function listLibraries(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
} catch (RuntimeException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'type' => ag(array_flip(Config::get('supported')), $client::class),
|
||||
'libraries' => $client->listLibraries(),
|
||||
'links' => [
|
||||
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
|
||||
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend\Library;
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\API\Backend\Index as BackendsIndex;
|
||||
use App\API\Backend\Index as backendIndex;
|
||||
use App\Commands\Backend\Library\MismatchCommand;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\DataUtil;
|
||||
@@ -19,11 +19,11 @@ final class Mismatched
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(BackendsIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backends.library.mismatched')]
|
||||
public function listLibraries(iRequest $request, array $args = []): iResponse
|
||||
#[Get(backendIndex::URL . '/{name:backend}/mismatched[/[{id}[/]]]', name: 'backend.mismatched')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$params = DataUtil::fromArray($request->getQueryParams());
|
||||
@@ -74,15 +74,6 @@ final class Mismatched
|
||||
$list[] = $processed;
|
||||
}
|
||||
}
|
||||
|
||||
$response = [
|
||||
'items' => $list,
|
||||
'links' => [
|
||||
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
|
||||
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $list);
|
||||
}
|
||||
}
|
||||
171
src/API/Backend/Option.php
Normal file
171
src/API/Backend/Option.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Libs\Attributes\Route\Delete;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Attributes\Route\Patch;
|
||||
use App\Libs\Attributes\Route\Post;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class Option
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option')]
|
||||
public function viewOption(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($option = ag($args, 'option'))) {
|
||||
return api_error('Invalid value for option path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
|
||||
if (false === $list->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $name . '.options.' . $option;
|
||||
|
||||
if (false === $list->has($key)) {
|
||||
return api_error(r("Option '{option}' not found in backend '{name}'.", [
|
||||
'option' => $option,
|
||||
'name' => $name
|
||||
]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$value = $list->get($key);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $option,
|
||||
'value' => $value,
|
||||
'type' => get_debug_type($value),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post(Index::URL . '/{name:backend}/option[/]', name: 'backend.option.add')]
|
||||
public function addOption(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
|
||||
if (false === $list->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$data = DataUtil::fromRequest($request);
|
||||
|
||||
if (null === ($option = $data->get('key'))) {
|
||||
return api_error('Invalid value for key.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$spec = require __DIR__ . '/../../../config/backend.spec.php';
|
||||
$found = false;
|
||||
|
||||
foreach ($spec as $supportedKey => $_) {
|
||||
if (str_ends_with($supportedKey, 'options.' . $option)) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $found) {
|
||||
return api_error(r("Option '{key}' is not supported.", ['key' => $option]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$value = $data->get('value');
|
||||
|
||||
$list->set($name . '.options.' . $option, $value)->persist();
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $option,
|
||||
'value' => $value,
|
||||
'type' => get_debug_type($value),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Patch(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option.update')]
|
||||
public function updateOption(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($option = ag($args, 'option'))) {
|
||||
return api_error('Invalid value for option parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
|
||||
if (false === $list->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $name . '.options.' . $option;
|
||||
if (false === $list->has($key)) {
|
||||
return api_error(r("Option '{option}' not found in backend '{name}'.", [
|
||||
'option' => $option,
|
||||
'name' => $name
|
||||
]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$data = DataUtil::fromRequest($request);
|
||||
|
||||
if (null === ($value = $data->get('value'))) {
|
||||
return api_error(r("No value was provided for '{key}'.", [
|
||||
'key' => $key,
|
||||
]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list->set($key, $value)->persist();
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $option,
|
||||
'value' => $value,
|
||||
'type' => get_debug_type($value),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Delete(Index::URL . '/{name:backend}/option/{option}[/]', name: 'backend.option.delete')]
|
||||
public function deleteOption(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($option = ag($args, 'option'))) {
|
||||
return api_error('Invalid value for option option parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
|
||||
if (false === $list->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $name . '.options.' . $option;
|
||||
|
||||
$value = $list->get($key);
|
||||
$list->delete($key)->persist();
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $option,
|
||||
'value' => $value,
|
||||
'type' => get_debug_type($value),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Libs\Attributes\Route\Patch;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use JsonException;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class PartialUpdate
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backends.view')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
|
||||
if (false === $list->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode((string)$request->getBody(), true, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]),
|
||||
HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
foreach ($data as $update) {
|
||||
if (!ag_exists($update, 'key')) {
|
||||
return api_error('No key to update was present.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$list->set($name . '.' . ag($update, 'key'), ag($update, 'value'));
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$list->persist();
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
|
||||
if (empty($backend)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
$backend = array_pop($backend);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'backend' => array_filter(
|
||||
$backend,
|
||||
fn($key) => false === in_array($key, ['options', 'webhook'], true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
),
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ final class Search
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backends.backend.search.id')]
|
||||
public function searchById(iRequest $request, array $args = []): iResponse
|
||||
#[Get(Index::URL . '/{name:backend}/search[/[{id}[/]]]', name: 'backend.search')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
@@ -55,14 +55,8 @@ final class Search
|
||||
]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
$response = [
|
||||
'results' => $id ? [$data] : $data,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'backend' => (string)$apiUrl->withPath(parseConfigValue(Index::URL . '/' . $name)),
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
'options' => [
|
||||
'raw' => (bool)$params->get('raw', false),
|
||||
],
|
||||
|
||||
@@ -18,11 +18,11 @@ final class Sessions
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/sessions[/]', name: 'backends.backend.sessions')]
|
||||
public function backendsView(iRequest $request, array $args = []): iResponse
|
||||
#[Get(Index::URL . '/{name:backend}/sessions[/]', name: 'backend.sessions')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -40,20 +40,9 @@ final class Sessions
|
||||
|
||||
try {
|
||||
$sessions = $client->getSessions($opts);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ag($sessions, 'sessions', []));
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$response = [
|
||||
'sessions' => ag($sessions, 'sessions', []),
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend\Library;
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\API\Backend\Index as BackendsIndex;
|
||||
use App\API\Backend\Index as backendIndex;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
@@ -18,11 +18,11 @@ final class Unmatched
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(BackendsIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backends.library.unmatched')]
|
||||
public function listLibraries(iRequest $request, array $args = []): iResponse
|
||||
#[Get(backendIndex::URL . '/{name:backend}/unmatched[/[{id}[/]]]', name: 'backend.unmatched')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('No backend was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$params = DataUtil::fromArray($request->getQueryParams());
|
||||
@@ -63,14 +63,6 @@ final class Unmatched
|
||||
}
|
||||
}
|
||||
|
||||
$response = [
|
||||
'items' => $list,
|
||||
'links' => [
|
||||
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
|
||||
'backend' => (string)parseConfigValue(BackendsIndex::URL . "/{$name}"),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $list);
|
||||
}
|
||||
}
|
||||
144
src/API/Backend/Update.php
Normal file
144
src/API/Backend/Update.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Backends\Common\ClientInterface as iClient;
|
||||
use App\Libs\Attributes\Route\Patch;
|
||||
use App\Libs\Attributes\Route\Put;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use JsonException;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class Update
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
private ConfigFile $backendFile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->backendFile = ConfigFile::open(Config::get('backends_file'), 'yaml', autoCreate: true);
|
||||
}
|
||||
|
||||
#[Put(Index::URL . '/{name:backend}[/]', name: 'backend.update')]
|
||||
public function update(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === $this->backendFile->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->backendFile->set(
|
||||
$name,
|
||||
$this->fromRequest($this->backendFile->get($name), $request, $this->getClient($name))
|
||||
)->persist();
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
|
||||
if (empty($backend)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$backend = array_pop($backend);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $backend);
|
||||
}
|
||||
|
||||
#[Patch(Index::URL . '/{name:backend}[/]', name: 'backend.patch')]
|
||||
public function patchUpdate(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for name path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === $this->backendFile->has($name)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode((string)$request->getBody(), true, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
return api_error(r('Invalid JSON data. {error}', ['error' => $e->getMessage()]),
|
||||
HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$spec = require __DIR__ . '/../../../config/backend.spec.php';
|
||||
|
||||
foreach ($data as $update) {
|
||||
$key = ag($update, 'key');
|
||||
$value = ag($update, 'value');
|
||||
|
||||
if (null === $key) {
|
||||
return api_error('No key to update was present.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === in_array($key, $spec, true)) {
|
||||
return api_error(r('Invalid key to update: {key}', ['key' => $key]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->backendFile->set("{$name}.{$key}", $value);
|
||||
}
|
||||
|
||||
$this->backendFile->persist();
|
||||
|
||||
$backend = $this->getBackends(name: $name);
|
||||
|
||||
if (empty($backend)) {
|
||||
return api_error(r("Backend '{name}' not found.", ['name' => $name]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$backend = array_pop($backend);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $backend);
|
||||
}
|
||||
|
||||
private function fromRequest(array $config, iRequest $request, iClient $client): array
|
||||
{
|
||||
$data = DataUtil::fromArray($request->getParsedBody());
|
||||
|
||||
$newData = [
|
||||
'url' => $data->get('url'),
|
||||
'token' => $data->get('token'),
|
||||
'user' => $data->get('user'),
|
||||
'uuid' => $data->get('uuid'),
|
||||
'export' => [
|
||||
'enabled' => (bool)$data->get('export.enabled', false),
|
||||
],
|
||||
'import' => [
|
||||
'enabled' => (bool)$data->get('import.enabled', false),
|
||||
],
|
||||
'webhook' => [
|
||||
'match' => [
|
||||
'user' => (bool)$data->get('webhook.match.user'),
|
||||
'uuid' => (bool)$data->get('webhook.match.uuid'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$optionals = [
|
||||
Options::DUMP_PAYLOAD => 'bool',
|
||||
Options::LIBRARY_SEGMENT => 'int',
|
||||
Options::IGNORE => 'string',
|
||||
];
|
||||
|
||||
foreach ($optionals as $key => $type) {
|
||||
if (null !== ($value = $data->get('options.' . $key))) {
|
||||
settype($value, $type);
|
||||
$newData = ag_set($newData, "options.{$key}", $value);
|
||||
}
|
||||
}
|
||||
|
||||
return deepArrayMerge([$config, $client->fromRequest($newData, $request)]);
|
||||
}
|
||||
}
|
||||
@@ -18,19 +18,13 @@ final class Users
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/users[/]', name: 'backends.backend.users')]
|
||||
public function backendsView(iRequest $request, array $args = []): iResponse
|
||||
#[Get(Index::URL . '/{name:backend}/users[/]', name: 'backend.users')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$opts = [];
|
||||
$params = DataUtil::fromRequest($request, true);
|
||||
|
||||
@@ -43,21 +37,11 @@ final class Users
|
||||
}
|
||||
|
||||
try {
|
||||
$users = $client->getUsersList($opts);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $this->getClient(name: $name)->getUsersList($opts));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$response = [
|
||||
'users' => $users,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\API\Backend;
|
||||
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Options;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
@@ -18,42 +16,19 @@ final class Version
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Get(Index::URL . '/{name:backend}/version[/]', name: 'backends.backend.info')]
|
||||
public function backendsView(iRequest $request, array $args = []): iResponse
|
||||
#[Get(Index::URL . '/{name:backend}/version[/]', name: 'backend.version')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getClient(name: $name);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['version' => $this->getClient(name: $name)->getVersion()]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$opts = [];
|
||||
$params = DataUtil::fromRequest($request, true);
|
||||
|
||||
if (true === (bool)$params->get('raw', false)) {
|
||||
$opts[Options::RAW_RESPONSE] = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$version = $client->getVersion($opts);
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$response = [
|
||||
'version' => $version,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,20 +27,20 @@ final class Webhooks
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
private iLogger $accessLog;
|
||||
private iLogger $logfile;
|
||||
|
||||
public function __construct(private iCache $cache)
|
||||
{
|
||||
$this->accessLog = new Logger(name: 'http', processors: [new LogMessageProcessor()]);
|
||||
$this->logfile = new Logger(name: 'webhook', processors: [new LogMessageProcessor()]);
|
||||
|
||||
$level = Config::get('webhook.debug') ? Level::Debug : Level::Info;
|
||||
|
||||
if (null !== ($logfile = Config::get('webhook.logfile'))) {
|
||||
$this->accessLog = $this->accessLog->pushHandler(new StreamHandler($logfile, $level, true));
|
||||
$this->logfile = $this->logfile->pushHandler(new StreamHandler($logfile, $level, true));
|
||||
}
|
||||
|
||||
if (true === inContainer()) {
|
||||
$this->accessLog->pushHandler(new StreamHandler('php://stderr', $level, true));
|
||||
$this->logfile->pushHandler(new StreamHandler('php://stderr', $level, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +53,27 @@ final class Webhooks
|
||||
* @return iResponse The response object.
|
||||
* @throws InvalidArgumentException if cache key is invalid.
|
||||
*/
|
||||
#[Route(['POST', 'PUT'], Index::URL . '/{name:backend}/webhook[/]', name: 'webhooks.receive')]
|
||||
#[Route(['POST', 'PUT'], Index::URL . '/{name:backend}/webhook[/]', name: 'backend.webhook')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($name = ag($args, 'name'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->process($name, $request)->withHeader('X-Log-Response', '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the incoming webhook request.
|
||||
*
|
||||
* @param string $name The backend name.
|
||||
* @param iRequest $request The incoming request object.
|
||||
*
|
||||
* @return iResponse The response object.
|
||||
* @throws InvalidArgumentException if cache key is invalid.
|
||||
*/
|
||||
private function process(string $name, iRequest $request): iResponse
|
||||
{
|
||||
try {
|
||||
$backend = $this->getBackends(name: $name);
|
||||
if (empty($backend)) {
|
||||
@@ -84,7 +98,7 @@ final class Webhooks
|
||||
if (null === ($requestUser = ag($attr, 'user.id'))) {
|
||||
$message = "Request payload didn't contain a user id. Backend requires a user check.";
|
||||
$this->write($request, Level::Info, $message);
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === hash_equals((string)$userId, (string)$requestUser)) {
|
||||
@@ -93,7 +107,7 @@ final class Webhooks
|
||||
'config_user' => $userId,
|
||||
]);
|
||||
$this->write($request, Level::Info, $message);
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +115,7 @@ final class Webhooks
|
||||
if (null === ($requestBackendId = ag($attr, 'backend.id'))) {
|
||||
$message = "Request payload didn't contain the backend unique id.";
|
||||
$this->write($request, Level::Info, $message);
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === hash_equals((string)$uuid, (string)$requestBackendId)) {
|
||||
@@ -110,7 +124,7 @@ final class Webhooks
|
||||
'config_uid' => $uuid,
|
||||
]);
|
||||
$this->write($request, Level::Info, $message);
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST)->withHeader('X-Log-Response', '0');
|
||||
return api_error($message, HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +142,7 @@ final class Webhooks
|
||||
'backend' => $client->getName(),
|
||||
]), forceContext: true);
|
||||
|
||||
return $response->withHeader('X-Log-Response', '0');
|
||||
return $response;
|
||||
}
|
||||
|
||||
$entity = $client->parseWebhook($request);
|
||||
@@ -151,7 +165,7 @@ final class Webhooks
|
||||
]
|
||||
);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED)->withHeader('X-Log-Response', '0');
|
||||
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED);
|
||||
}
|
||||
|
||||
if ((0 === (int)$entity->episode || null === $entity->season) && $entity->isEpisode()) {
|
||||
@@ -170,7 +184,7 @@ final class Webhooks
|
||||
]
|
||||
);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED)->withHeader('X-Log-Response', '0');
|
||||
return api_response(HTTP_STATUS::HTTP_NOT_MODIFIED);
|
||||
}
|
||||
|
||||
$items = $this->cache->get('requests', []);
|
||||
@@ -211,7 +225,7 @@ final class Webhooks
|
||||
]
|
||||
);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK)->withHeader('X-Log-Response', '0');
|
||||
return api_response(HTTP_STATUS::HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,9 +271,9 @@ final class Webhooks
|
||||
}
|
||||
|
||||
if (true === (Config::get('logs.context') || $forceContext)) {
|
||||
$this->accessLog->log($level, $message, $context);
|
||||
$this->logfile->log($level, $message, $context);
|
||||
} else {
|
||||
$this->accessLog->log($level, r($message, $context));
|
||||
$this->logfile->log($level, r($message, $context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\API\Backends;
|
||||
|
||||
use App\Backends\Common\Cache as BackendCache;
|
||||
use App\Backends\Common\ClientInterface;
|
||||
use App\Backends\Common\ClientInterface as iClient;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Libs\Attributes\Route\Post;
|
||||
use App\Libs\Config;
|
||||
@@ -80,7 +80,7 @@ final class Add
|
||||
}
|
||||
|
||||
$instance = Container::getNew($class);
|
||||
assert($instance instanceof ClientInterface, new RuntimeException('Invalid client class.'));
|
||||
assert($instance instanceof iClient, new RuntimeException('Invalid client class.'));
|
||||
|
||||
try {
|
||||
$config = DataUtil::fromArray($this->fromRequest($type, $request, $instance));
|
||||
@@ -118,15 +118,10 @@ final class Add
|
||||
$data = $this->getBackends(name: $name);
|
||||
$data = array_pop($data);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_CREATED, [
|
||||
...$data,
|
||||
'links' => [
|
||||
'self' => parseConfigValue(Index::URL) . '/' . $name,
|
||||
],
|
||||
]);
|
||||
return api_response(HTTP_STATUS::HTTP_CREATED, $data);
|
||||
}
|
||||
|
||||
private function fromRequest(string $type, iRequest $request, ClientInterface|null $client = null): array
|
||||
private function fromRequest(string $type, iRequest $request, iClient $client): array
|
||||
{
|
||||
$data = DataUtil::fromArray($request->getParsedBody());
|
||||
|
||||
@@ -162,16 +157,11 @@ final class Add
|
||||
|
||||
foreach ($optionals as $key => $type) {
|
||||
if (null !== ($value = $data->get('options.' . $key))) {
|
||||
$val = $data->get($value, $type);
|
||||
settype($val, $type);
|
||||
$config = ag_set($config, "options.{$key}", $val);
|
||||
settype($value, $type);
|
||||
$config = ag_set($config, "options.{$key}", $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $client) {
|
||||
$config = $client->fromRequest($config, $request);
|
||||
}
|
||||
|
||||
return $config;
|
||||
return $client->fromRequest($config, $request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,36 +23,19 @@ final class Index
|
||||
'options.' . Options::ADMIN_TOKEN
|
||||
];
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'backends.index')]
|
||||
#[Get(self::URL . '[/]', name: 'backends')]
|
||||
public function __invoke(iRequest $request): iResponse
|
||||
{
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
$urlPath = $request->getUri()->getPath();
|
||||
|
||||
$response = [
|
||||
'backends' => [],
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
],
|
||||
];
|
||||
$list = [];
|
||||
|
||||
foreach ($this->getBackends() as $backend) {
|
||||
$backend = array_filter(
|
||||
$list[] = array_filter(
|
||||
$backend,
|
||||
fn($key) => false === in_array($key, ['options', 'webhook'], true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
$backend['links'] = [
|
||||
'self' => (string)$apiUrl->withPath(
|
||||
parseConfigValue(\App\API\Backend\Index::URL) . '/' . $backend['name']
|
||||
),
|
||||
];
|
||||
|
||||
$response['backends'][] = $backend;
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $response);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
38
src/API/Backends/UUid.php
Normal file
38
src/API/Backends/UUid.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backends;
|
||||
|
||||
use App\Libs\Attributes\Route\Route;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class UUid
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Route(['GET', 'POST'], Index::URL . '/uuid/{type}[/]', name: 'backends.get.unique_id')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($type = ag($args, 'type'))) {
|
||||
return api_error('Invalid value for type path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getBasicClient($type, DataUtil::fromRequest($request, true));
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'identifier' => $client->getIdentifier(true)
|
||||
]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/API/Backends/Users.php
Normal file
47
src/API/Backends/Users.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Backends;
|
||||
|
||||
use App\Libs\Attributes\Route\Route;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use App\Libs\Traits\APITraits;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Throwable;
|
||||
|
||||
final class Users
|
||||
{
|
||||
use APITraits;
|
||||
|
||||
#[Route(['GET', 'POST'], Index::URL . '/users/{type}[/]', name: 'backends.get.users')]
|
||||
public function __invoke(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($type = ag($args, 'type'))) {
|
||||
return api_error('Invalid value for type path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getBasicClient($type, DataUtil::fromRequest($request, true));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$users = [];
|
||||
foreach ($client->getUsersList() as $user) {
|
||||
$users[] = [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name']
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $users);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ final class Index
|
||||
$this->pdo = $this->db->getPDO();
|
||||
}
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'history.index')]
|
||||
#[Get(self::URL . '[/]', name: 'history')]
|
||||
public function historyIndex(iRequest $request): iResponse
|
||||
{
|
||||
$es = fn(string $val) => $this->db->identifier($val);
|
||||
@@ -334,22 +334,12 @@ final class Index
|
||||
return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$item = $item->getAll();
|
||||
|
||||
$item[iState::COLUMN_WATCHED] = $entity->isWatched();
|
||||
$item[iState::COLUMN_UPDATED] = makeDate($entity->updated);
|
||||
|
||||
$item = [
|
||||
...$item,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['history' => $item]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ final class Index
|
||||
private const int DEFAULT_LIMIT = 1000;
|
||||
private int $counter = 1;
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'logs.list')]
|
||||
#[Get(self::URL . '[/]', name: 'logs')]
|
||||
public function logsList(iRequest $request): iResponse
|
||||
{
|
||||
$path = fixPath(Config::get('tmpDir') . '/logs');
|
||||
@@ -38,7 +38,6 @@ final class Index
|
||||
|
||||
foreach (glob($path . '/*.*.log') as $file) {
|
||||
preg_match('/(\w+)\.(\w+)\.log/i', basename($file), $matches);
|
||||
$url = $apiUrl->withPath(parseConfigValue(self::URL . "/" . basename($file)));
|
||||
|
||||
$builder = [
|
||||
'filename' => basename($file),
|
||||
@@ -46,23 +45,19 @@ final class Index
|
||||
'date' => $matches[2] ?? '??',
|
||||
'size' => filesize($file),
|
||||
'modified' => makeDate(filemtime($file)),
|
||||
'urls' => [
|
||||
'self' => (string)$url,
|
||||
'stream' => (string)$url->withQuery($query),
|
||||
],
|
||||
];
|
||||
|
||||
$list[] = $builder;
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['logs' => $list]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, $list);
|
||||
}
|
||||
|
||||
#[Get(Index::URL . '/{filename}[/]', name: 'logs.view')]
|
||||
public function logView(iRequest $request, array $args = []): iResponse
|
||||
{
|
||||
if (null === ($filename = ag($args, 'filename'))) {
|
||||
return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
return api_error('Invalid value for filename path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$path = realpath(fixPath(Config::get('tmpDir') . '/logs'));
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\Plex;
|
||||
|
||||
use App\Backends\Plex\PlexClient;
|
||||
use App\Libs\Attributes\Route\Post;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
|
||||
final class Discover
|
||||
{
|
||||
public const string URL = '%{api.prefix}/plex/discover';
|
||||
|
||||
public function __construct(private iHttp $http)
|
||||
{
|
||||
}
|
||||
|
||||
#[Post(self::URL . '[/]', name: 'plex.discover')]
|
||||
public function plexDiscover(iRequest $request): iResponse
|
||||
{
|
||||
$data = DataUtil::fromArray($request->getParsedBody());
|
||||
|
||||
if (null === ($token = $data->get('token'))) {
|
||||
return api_error('No token was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$list = PlexClient::discover($this->http, $token);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ag($list, 'list', []));
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,11 @@ final class Env
|
||||
'WS_CACHE_URL'
|
||||
];
|
||||
|
||||
private EnvFile $envfile;
|
||||
private EnvFile $envFile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->envfile = (new EnvFile(file: Config::get('path') . '/config/.env', create: true));
|
||||
$this->envFile = (new EnvFile(file: Config::get('path') . '/config/.env', create: true));
|
||||
}
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'system.env')]
|
||||
@@ -35,12 +35,9 @@ final class Env
|
||||
$response = [
|
||||
'data' => [],
|
||||
'file' => Config::get('path') . '/config/.env',
|
||||
'links' => [
|
||||
'self' => (string)$request->getUri()->withHost('')->withPort(0)->withScheme(''),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->envfile->getAll() as $key => $val) {
|
||||
foreach ($this->envFile->getAll() as $key => $val) {
|
||||
if (false === str_starts_with($key, 'WS_')) {
|
||||
continue;
|
||||
}
|
||||
@@ -49,9 +46,6 @@ final class Env
|
||||
'key' => $key,
|
||||
'value' => $val,
|
||||
'mask' => in_array($key, self::MASK),
|
||||
'urls' => [
|
||||
'self' => (string)$request->getUri()->withPath(parseConfigValue(self::URL . '/' . $key)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -70,13 +64,13 @@ final class Env
|
||||
return api_error(r("Invalid key '{key}' was given.", ['key' => $key]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === $this->envfile->has($key)) {
|
||||
if (false === $this->envFile->has($key)) {
|
||||
return api_error(r("Key '{key}' is not set.", ['key' => $key]), HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $key,
|
||||
'value' => $this->envfile->get($key),
|
||||
'value' => $this->envFile->get($key),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -94,7 +88,7 @@ final class Env
|
||||
}
|
||||
|
||||
if ('DELETE' === $request->getMethod()) {
|
||||
$this->envfile->remove($key);
|
||||
$this->envFile->remove($key);
|
||||
} else {
|
||||
$params = DataUtil::fromRequest($request);
|
||||
if (null === ($value = $params->get('value', null))) {
|
||||
@@ -103,14 +97,14 @@ final class Env
|
||||
]), HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->envfile->set($key, $value);
|
||||
$this->envFile->set($key, $value);
|
||||
}
|
||||
|
||||
$this->envfile->persist();
|
||||
$this->envFile->persist();
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'key' => $key,
|
||||
'value' => env($key, fn() => $this->envfile->get($key)),
|
||||
'value' => $this->envFile->get($key, fn() => env($key)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ final class Supported
|
||||
#[Get(self::URL . '[/]', name: 'system.supported')]
|
||||
public function __invoke(iRequest $request): iResponse
|
||||
{
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'supported' => array_keys(Config::get('supported')),
|
||||
]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['supported' => array_keys(Config::get('supported'))]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Commands\System\TasksCommand;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Attributes\Route\Route;
|
||||
use App\Libs\HTTP_STATUS;
|
||||
use Cron\CronExpression;
|
||||
use DateInterval;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
@@ -28,29 +29,15 @@ final class Index
|
||||
#[Get(self::URL . '[/]', name: 'tasks.index')]
|
||||
public function tasksIndex(iRequest $request): iResponse
|
||||
{
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
$urlPath = rtrim($request->getUri()->getPath(), '/');
|
||||
|
||||
$queuedTasks = $this->cache->get('queued_tasks', []);
|
||||
|
||||
$response = [
|
||||
'tasks' => [],
|
||||
'queued' => $queuedTasks,
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
],
|
||||
];
|
||||
|
||||
foreach (TasksCommand::getTasks() as $task) {
|
||||
$task = self::formatTask($task);
|
||||
|
||||
$task['links'] = [
|
||||
'self' => (string)$apiUrl->withPath($urlPath . '/' . ag($task, 'name')),
|
||||
'queue' => (string)$apiUrl->withPath($urlPath . '/' . ag($task, 'name') . '/queue'),
|
||||
];
|
||||
|
||||
$task['queued'] = in_array(ag($task, 'name'), $queuedTasks);
|
||||
|
||||
$response['tasks'][] = $task;
|
||||
}
|
||||
|
||||
@@ -87,17 +74,9 @@ final class Index
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['queue' => $queuedTasks]);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('')->withUserInfo('');
|
||||
$urlPath = parseConfigValue(Index::URL);
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, [
|
||||
'task' => $id,
|
||||
'is_queued' => in_array($id, $queuedTasks),
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'task' => (string)$apiUrl->withPath($urlPath . '/' . $id),
|
||||
'tasks' => (string)$apiUrl->withPath($urlPath),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -108,34 +87,27 @@ final class Index
|
||||
return api_error('No id was given.', HTTP_STATUS::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme('');
|
||||
|
||||
$task = TasksCommand::getTasks($id);
|
||||
|
||||
if (empty($task)) {
|
||||
return api_error('Task not found.', HTTP_STATUS::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response = [
|
||||
...Index::formatTask($task),
|
||||
'links' => [
|
||||
'self' => (string)$apiUrl,
|
||||
'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)),
|
||||
],
|
||||
];
|
||||
|
||||
return api_response(HTTP_STATUS::HTTP_OK, ['task' => $response]);
|
||||
return api_response(HTTP_STATUS::HTTP_OK, Index::formatTask($task));
|
||||
}
|
||||
|
||||
private function formatTask(array $task): array
|
||||
{
|
||||
$isEnabled = (bool)ag($task, 'enabled', false);
|
||||
|
||||
$timer = ag($task, 'timer');
|
||||
assert($timer instanceof CronExpression);
|
||||
|
||||
$item = [
|
||||
'name' => ag($task, 'name'),
|
||||
'description' => ag($task, 'description'),
|
||||
'enabled' => true === $isEnabled,
|
||||
'timer' => ag($task, 'timer')->getexpression(),
|
||||
'timer' => $timer->getExpression(),
|
||||
'next_run' => null,
|
||||
'prev_run' => null,
|
||||
'command' => ag($task, 'command'),
|
||||
@@ -147,8 +119,13 @@ final class Index
|
||||
}
|
||||
|
||||
if (true === $isEnabled) {
|
||||
$item['next_run'] = makeDate(ag($task, 'timer')->getNextRunDate());
|
||||
$item['prev_run'] = makeDate(ag($task, 'timer')->getPreviousRunDate());
|
||||
try {
|
||||
$item['next_run'] = makeDate($timer->getNextRunDate());
|
||||
$item['prev_run'] = makeDate($timer->getPreviousRunDate());
|
||||
} catch (\Exception) {
|
||||
$item['next_run'] = null;
|
||||
$item['prev_run'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $item;
|
||||
|
||||
@@ -41,6 +41,13 @@ interface ClientInterface
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get The Backend type.
|
||||
*
|
||||
* @return string Backend type.
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Inject logger.
|
||||
*
|
||||
|
||||
@@ -163,6 +163,11 @@ class EmbyClient implements iClient
|
||||
return $this->context?->backendName ?? static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -180,6 +180,11 @@ class JellyfinClient implements iClient
|
||||
return $this->context?->backendName ?? static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Backends\Common\Levels;
|
||||
use App\Backends\Common\Response;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Options;
|
||||
use DateInterval;
|
||||
use JsonException;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -40,7 +41,7 @@ final class GetUsersList
|
||||
{
|
||||
return $this->tryResponse(
|
||||
context: $context,
|
||||
fn: fn() => $this->getUsers($context, $opts),
|
||||
fn: fn() => $this->action($context, $opts),
|
||||
action: $this->action
|
||||
);
|
||||
}
|
||||
@@ -51,7 +52,26 @@ final class GetUsersList
|
||||
* @throws ExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function getUsers(Context $context, array $opts = []): Response
|
||||
private function action(Context $context, array $opts = []): Response
|
||||
{
|
||||
$cls = fn() => $this->real_request($context, $opts);
|
||||
|
||||
return true === (bool)ag($opts, Options::NO_CACHE) ? $cls() : $this->tryCache(
|
||||
$context,
|
||||
'users_' . md5(json_encode($opts)),
|
||||
$cls,
|
||||
new DateInterval('PT5M'),
|
||||
$this->logger
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Users list.
|
||||
*
|
||||
* @throws ExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function real_request(Context $context, array $opts = []): Response
|
||||
{
|
||||
$url = Container::getNew(UriInterface::class)->withPort(443)->withScheme('https')->withHost('plex.tv')
|
||||
->withPath('/api/v2/home/users/');
|
||||
@@ -77,6 +97,7 @@ final class GetUsersList
|
||||
context: [
|
||||
'backend' => $context->backendName,
|
||||
'status_code' => $response->getStatusCode(),
|
||||
'body' => $response->getContent(),
|
||||
],
|
||||
level: Levels::ERROR
|
||||
),
|
||||
@@ -137,4 +158,5 @@ final class GetUsersList
|
||||
|
||||
return new Response(status: true, response: $list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -180,6 +180,11 @@ class PlexClient implements iClient
|
||||
return $this->context?->backendName ?? static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::CLIENT_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@@ -558,12 +563,16 @@ class PlexClient implements iClient
|
||||
{
|
||||
$params = DataUtil::fromArray($request->getParsedBody());
|
||||
|
||||
if (null !== ($uuid = $params->get('options.' . Options::PLEX_USER_UUID))) {
|
||||
$config = ag_set($config, 'options.' . Options::PLEX_USER_UUID, $uuid);
|
||||
if (null !== ($val = $params->get('options.' . Options::PLEX_USER_UUID))) {
|
||||
$config = ag_set($config, 'options.' . Options::PLEX_USER_UUID, $val);
|
||||
}
|
||||
|
||||
if (null !== ($adminToken = $params->get('options.' . Options::ADMIN_TOKEN))) {
|
||||
$config = ag_set($config, 'options.' . Options::ADMIN_TOKEN, $adminToken);
|
||||
if (null !== ($val = $params->get('options.' . Options::ADMIN_TOKEN))) {
|
||||
$config = ag_set($config, 'options.' . Options::ADMIN_TOKEN, $val);
|
||||
}
|
||||
|
||||
if (null !== ($val = $params->get('options.' . Options::PLEX_USE_OLD_PROGRESS_ENDPOINT))) {
|
||||
$config = ag_set($config, 'options.' . Options::PLEX_USE_OLD_PROGRESS_ENDPOINT, (bool)$val);
|
||||
}
|
||||
|
||||
if (null !== ($userId = ag($config, 'user')) && !is_int($userId)) {
|
||||
|
||||
@@ -25,21 +25,22 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
#[Cli(command: self::ROUTE)]
|
||||
final class LogsCommand extends Command
|
||||
{
|
||||
public const ROUTE = 'system:logs';
|
||||
public const string ROUTE = 'system:logs';
|
||||
|
||||
/**
|
||||
* @var array Constant array containing names of supported log files.
|
||||
*/
|
||||
private const LOG_FILES = [
|
||||
private const array LOG_FILES = [
|
||||
'app',
|
||||
'access',
|
||||
'task'
|
||||
'task',
|
||||
'webhook',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int The default limit of how many lines to show.
|
||||
*/
|
||||
public const DEFAULT_LIMIT = 50;
|
||||
public const int DEFAULT_LIMIT = 50;
|
||||
|
||||
/**
|
||||
* Configure the command.
|
||||
|
||||
@@ -22,9 +22,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
#[Cli(command: self::ROUTE)]
|
||||
final class PruneCommand extends Command
|
||||
{
|
||||
public const ROUTE = 'system:prune';
|
||||
public const string ROUTE = 'system:prune';
|
||||
|
||||
public const TASK_NAME = 'prune';
|
||||
public const string TASK_NAME = 'prune';
|
||||
|
||||
/**
|
||||
* Class Constructor.
|
||||
|
||||
@@ -477,13 +477,12 @@ final class Initializer
|
||||
{
|
||||
$inContainer = inContainer();
|
||||
|
||||
if (null !== ($logfile = Config::get('webhook.logfile'))) {
|
||||
$level = Config::get('webhook.debug') ? Level::Debug : Level::Info;
|
||||
if (null !== ($logfile = Config::get('api.logfile'))) {
|
||||
$this->accessLog = $logger->withName(name: 'http')
|
||||
->pushHandler(new StreamHandler($logfile, $level, true));
|
||||
->pushHandler(new StreamHandler($logfile, Level::Info, true));
|
||||
|
||||
if (true === $inContainer) {
|
||||
$this->accessLog->pushHandler(new StreamHandler('php://stderr', $level, true));
|
||||
$this->accessLog->pushHandler(new StreamHandler('php://stderr', Level::Info, true));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ final class APIKeyRequiredMiddleware implements MiddlewareInterface
|
||||
|
||||
private const array OPEN_ROUTES = [
|
||||
HealthCheck::URL,
|
||||
'/webhook'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -30,9 +31,11 @@ final class APIKeyRequiredMiddleware implements MiddlewareInterface
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$requestPath = rtrim($request->getUri()->getPath(), '/');
|
||||
|
||||
foreach (self::OPEN_ROUTES as $route) {
|
||||
$route = parseConfigValue($route);
|
||||
if (true === str_starts_with($request->getUri()->getPath(), parseConfigValue($route))) {
|
||||
$route = rtrim(parseConfigValue($route), '/');
|
||||
if (true === str_starts_with($requestPath, $route) || true === str_ends_with($requestPath, $route)) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Libs\Traits;
|
||||
|
||||
use App\Backends\Common\Cache as BackendCache;
|
||||
use App\Backends\Common\ClientInterface;
|
||||
use App\Backends\Common\ClientInterface as iClient;
|
||||
use App\Backends\Common\Context;
|
||||
use App\Libs\Config;
|
||||
use App\Libs\ConfigFile;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Exceptions\InvalidArgumentException;
|
||||
use App\Libs\Exceptions\RuntimeException;
|
||||
use App\Libs\Uri;
|
||||
|
||||
trait APITraits
|
||||
{
|
||||
@@ -66,4 +73,42 @@ trait APITraits
|
||||
|
||||
return $backends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic client to inquiry about the backend.
|
||||
*
|
||||
* @param string $type Backend type.
|
||||
* @param DataUtil $data The request data.
|
||||
*
|
||||
* @return iClient The client instance.
|
||||
* @throws InvalidArgumentException If url, token is missing or type is incorrect.
|
||||
*/
|
||||
protected function getBasicClient(string $type, DataUtil $data): iClient
|
||||
{
|
||||
if (null === ($class = Config::get("supported.{$type}", null))) {
|
||||
throw new InvalidArgumentException(r("Unexpected client type '{type}' was given.", ['type' => $type]));
|
||||
}
|
||||
|
||||
if (null === $data->get('url')) {
|
||||
throw new InvalidArgumentException('No URL was given.');
|
||||
}
|
||||
|
||||
if (null === $data->get('token')) {
|
||||
throw new InvalidArgumentException('No token was given.');
|
||||
}
|
||||
|
||||
$instance = Container::getNew($class);
|
||||
assert($instance instanceof ClientInterface, new InvalidArgumentException('Invalid client class.'));
|
||||
return $instance->withContext(
|
||||
new Context(
|
||||
clientName: $type,
|
||||
backendName: 'basic_' . $type,
|
||||
backendUrl: new Uri($data->get('url')),
|
||||
cache: Container::get(BackendCache::class),
|
||||
backendId: $data->get('uuid'),
|
||||
backendToken: $data->get('token'),
|
||||
backendUser: $data->get('user'),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ if (!function_exists('api_response')) {
|
||||
return (new Response(
|
||||
status: $status->value,
|
||||
headers: $headers,
|
||||
body: $body ? json_encode(
|
||||
body: null !== $body ? json_encode(
|
||||
$body,
|
||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
|
||||
) : null,
|
||||
@@ -1253,3 +1253,27 @@ if (!function_exists('addCors')) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('deepArrayMerge')) {
|
||||
function deepArrayMerge(array $arrays, $preserve_integer_keys = false)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($arrays as $array) {
|
||||
foreach ($array as $key => $value) {
|
||||
// Renumber integer keys as array_merge_recursive() does unless
|
||||
// $preserve_integer_keys is set to TRUE. Note that PHP automatically
|
||||
// converts array keys that are integer strings (e.g., '1') to integers.
|
||||
if (is_int($key) && !$preserve_integer_keys) {
|
||||
$result[] = $value;
|
||||
} // Recurse when both values are arrays.
|
||||
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
|
||||
$result[$key] = deepArrayMerge([$result[$key], $value], $preserve_integer_keys);
|
||||
} // Otherwise, use the latter value, overriding any previous value.
|
||||
else {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user