Standardizing how we report the items via API, there are more work to be done.

This commit is contained in:
abdulmohsen
2024-06-15 23:47:24 +03:00
parent 280d30a68d
commit ca32278a52
20 changed files with 320 additions and 351 deletions

View File

@@ -71,7 +71,7 @@
<i class="fas fa-eye-slash" v-if="!history.watched"></i>
<i class="fas fa-eye" v-else></i>
</span>
<NuxtLink :to="`/history/${history.id}`" v-text="history.full_title ?? history.title"/>
<NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
@@ -83,7 +83,9 @@
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
{{ moment(history.updated).fromNow() }}
<span class="has-tooltip" v-tooltip="moment.unix(history.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(history.updated).fromNow() }}
</span>
</span>
</div>
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
@@ -136,7 +138,7 @@
<script setup>
import moment from 'moment'
import Message from '~/components/Message.vue'
import {formatDuration, notification} from "~/utils/index.js";
import {formatDuration, makeName, notification} from "~/utils/index.js";
const backend = useRoute().params.backend
@@ -150,7 +152,7 @@ const loadRecentHistory = async () => {
search.append('perpage', 6)
search.append('key', 'metadata')
search.append('q', `${backend}.via://${backend}`)
search.append('sort', `updated:desc`)
search.append('sort', `created_at:desc`)
const response = await request(`/history/?${search.toString()}`)
const json = await response.json()

View File

@@ -108,7 +108,7 @@
<div class="card" :class="{ 'is-success': item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow">
<NuxtLink :to="item.url" v-text="item.full_title ?? item.title" target="_blank"/>
<NuxtLink :to="item.url" v-text="makeName(item)" target="_blank"/>
</p>
<span class="card-header-icon">
<span class="icon">
@@ -123,16 +123,14 @@
<div class="is-text-overflow is-clickable"
@click="(e) => e.target.classList.toggle('is-text-overflow')">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Title:&nbsp;</span>
{{ item.title }}
</div>
</div>
<div class="column is-12 is-clickable has-text-left" v-if="item?.path"
<div class="column is-12 is-clickable has-text-left" v-if="item?.content_path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">Path:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/>
<NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/>
</div>
</div>
</div>
@@ -141,7 +139,9 @@
<div class="card-footer-item">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
{{ moment.unix(item.updated).fromNow() }}
<span class="has-tooltip" v-tooltip="moment.unix(item.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(item.updated).fromNow() }}
</span>
</span>
</div>
<div class="card-footer-item">
@@ -196,7 +196,7 @@
<script setup>
import request from '~/utils/request.js'
import moment from 'moment'
import {makeSearchLink, notification} from '~/utils/index.js'
import {makeName, makeSearchLink, notification} from '~/utils/index.js'
import Message from "~/components/Message.vue";
import {useStorage} from "@vueuse/core";

View File

@@ -30,11 +30,11 @@
</p>
</div>
</div>
<div class="subtitle is-5" v-if="data?.via && headerTitle !== data?.title">
<div class="subtitle is-5" v-if="data?.via && data.content_title">
<span class="is-unselectable icon">
<i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }"></i>
</span>
{{ data?.title }}
{{ data?.content_title }}
</div>
</div>
@@ -137,8 +137,8 @@
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip" v-tooltip="moment(data.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment(data.updated).fromNow() }}
<span class="has-tooltip" v-tooltip="moment.unix(data.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(data.updated).fromNow() }}
</span>
</span>
</span>
@@ -176,7 +176,7 @@
<div class="column is-12" v-if="data.guids && Object.keys(data.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:</span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( data.type, source.split('guid_')[1], guid, data)">
@@ -188,7 +188,7 @@
<div class="column is-12" v-if="data.rguids && Object.keys(data.rguids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Relative Globally unique identifier for this episode'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>rGUIDs:</span>
<span>rGUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.rguids">
<NuxtLink :to="makeSearchLink('rguid', `${source.split('guid_')[1]}://${guid}`)">
@@ -200,7 +200,7 @@
<div class="column is-12" v-if="data.parent && Object.keys(data.parent).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:</span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in data.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, data)">
@@ -209,19 +209,19 @@
</span>
</div>
<div class="column is-12" v-if="data?.title">
<div class="column is-12" v-if="data?.content_title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Title:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', data.title)" v-text="data.title"/>
<span class="is-hidden-mobile">Subtitle:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', data.content_title)" v-text="data.content_title"/>
</div>
</div>
<div class="column is-12" v-if="data?.path">
<div class="column is-12" v-if="data?.content_path">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', data.path)" v-text="data.path"/>
<NuxtLink :to="makeSearchLink('path', data.content_path)" v-text="data.content_path"/>
</div>
</div>
@@ -274,8 +274,7 @@
<div class="column is-6">
<span class="icon-text">
<span class="icon">
<i class="fas fa-eye-slash" v-if="!parseInt(item.watched)"></i>
<i class="fas fa-eye" v-else></i>
<i class="fas fa-eye-slash" :class="parseInt(item.watched) ?'fa-eye-slash' : 'fa-eye'"></i>
</span>
<span>
<span class="is-hidden-mobile">Status:</span>
@@ -300,8 +299,8 @@
<span>
<span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip"
v-tooltip="moment(ag(data.extra, `${key}.received_at`, data.updated)).format('YYYY-MM-DD h:mm:ss A')">
{{ moment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
v-tooltip="getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format('YYYY-MM-DD h:mm:ss A')">
{{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
</span>
</span>
</span>
@@ -341,7 +340,7 @@
<div class="column is-12" v-if="item.guids && Object.keys(item.guids).length>0">
<span class="icon-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>GUIDs:</span>
<span>GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.guids">
<NuxtLink target="_blank" :to="makeGUIDLink( item.type, source.split('guid_')[1], guid, item)">
@@ -353,7 +352,7 @@
<div class="column is-12" v-if="item.parent && Object.keys(item.parent).length>0">
<span class="is-clickable icon-text" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Series GUIDs:</span>
<span>Series GUIDs:&nbsp;</span>
</span>
<span class="tag mr-1" v-for="(guid,source) in item.parent">
<NuxtLink target="_blank" :to="makeGUIDLink( 'series', source.split('guid_')[1], guid, item)">
@@ -365,7 +364,7 @@
<div class="column is-12" v-if="item?.extra?.title">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Title:&nbsp;</span>
<span class="is-hidden-mobile">Subtitle:&nbsp</span>
<NuxtLink :to="makeSearchLink('subtitle', item.extra.title)" v-text="item.extra.title"/>
</div>
</div>
@@ -394,7 +393,7 @@
</span>
</span>
<div v-if="showRawData" class="mt-2">
<pre><code>{{ JSON.stringify(data, null, 2) }}</code></pre>
<code class="is-block is-pre-wrap">{{ JSON.stringify(data, null, 2) }}</code>
</div>
</div>
@@ -435,7 +434,7 @@
<script setup>
import request from '~/utils/request.js'
import {ag, formatDuration, makeGUIDLink, makeSearchLink, notification, ucFirst} from '~/utils/index.js'
import {ag, formatDuration, makeGUIDLink, makeName, makeSearchLink, notification, ucFirst} from '~/utils/index.js'
import moment from 'moment'
import {useStorage} from "@vueuse/core";
import Message from "~/components/Message.vue";
@@ -444,7 +443,7 @@ const id = useRoute().params.id
useHead({title: `History : ${id}`})
const isLoading = ref(false)
const isLoading = ref(true)
const showRawData = ref(false)
const show_page_tips = useStorage('show_page_tips', true)
const show_history_page_warning = useStorage('show_history_page_warning', true)
@@ -480,7 +479,7 @@ const loadContent = async (id) => {
data.value = json
data.value._toggle = true
useHead({title: `History : ${json.full_title ?? json.title ?? id}`})
useHead({title: `History : ${makeName(json) ?? id}`})
}
const deleteItem = async (item) => {
@@ -488,7 +487,7 @@ const deleteItem = async (item) => {
return
}
if (!confirm(`Are you sure you want to delete '${item?.full_title ?? item.title ?? id}'?`)) {
if (!confirm(`Are you sure you want to delete '${makeName(item)}'?`)) {
return
}
@@ -503,7 +502,7 @@ const deleteItem = async (item) => {
return
}
notification('success', 'Success!', `Deleted '${item.full_title ?? item.title ?? id}'.`)
notification('success', 'Success!', `Deleted '${makeName(item)}'.`)
await navigateTo({name: 'history'})
} catch (e) {
notification('error', 'Error', e.message)
@@ -516,7 +515,7 @@ const toggleWatched = async () => {
if (!data.value) {
return
}
if (!confirm(`Mark '${data.value.full_title}' as ${data.value.watched ? 'unplayed' : 'played'}?`)) {
if (!confirm(`Mark '${makeName(data.value)}' as ${data.value.watched ? 'unplayed' : 'played'}?`)) {
return
}
try {
@@ -532,14 +531,15 @@ const toggleWatched = async () => {
}
data.value = json
notification('success', '', `Marked '${data.value.full_title}' as ${data.value.watched ? 'played' : 'unplayed'}`)
notification('success', '', `Marked '${makeName(data.value)}' as ${data.value.watched ? 'played' : 'unplayed'}`)
} catch (e) {
notification('error', 'Error', `Failed to update watched status. ${e}`)
notification('error', 'Error', `Request error. ${e}`)
}
}
const headerTitle = computed(() => `${data.value?.full_title ?? data.value?.title ?? id}`)
const getMoment = (time) => time.toString().length < 13 ? moment.unix(time) : moment(time)
const headerTitle = computed(() => isLoading.value ? id : makeName(data.value))
onMounted(async () => loadContent(id))
</script>

View File

@@ -125,7 +125,7 @@
<span class="icon" v-if="!item.progress">
<i class="fas" :class="{'fa-eye-slash': !item.watched, 'fa-eye': item.watched}"></i>
</span>
<NuxtLink :to="'/history/'+item.id" v-text="item.full_title ?? item.title"/>
<NuxtLink :to="'/history/'+item.id" v-text="makeName(item)"/>
</p>
<span class="card-header-icon">
<span class="icon">
@@ -135,28 +135,27 @@
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-12 has-text-left" v-if="item?.title">
<div class="column is-12 has-text-left" v-if="item?.content_title">
<div class="is-text-overflow is-clickable"
@click="(e) => e.target.classList.toggle('is-text-overflow')">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Title:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', item.title)"
@click="triggerSearch('subtitle', item.title)" v-text="item.title"/>
<NuxtLink :to="makeSearchLink('subtitle', item.content_title)"
@click="triggerSearch('subtitle', item.content_title)" v-text="item.content_title"/>
</div>
</div>
<div class="column is-12 is-clickable has-text-left" v-if="item?.path"
<div class="column is-12 is-clickable has-text-left" v-if="item?.content_path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">Path:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', item.path)" @click="triggerSearch('path', item.path)"
v-text="item.path"/>
<NuxtLink :to="makeSearchLink('path', item.content_path)"
@click="triggerSearch('path', item.content_path)"
v-text="item?.content_path"/>
</div>
</div>
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
{{ moment(item.updated).fromNow() }}
{{ moment.unix(item.updated_at).fromNow() }}
</span>
</div>
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
@@ -170,7 +169,7 @@
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>{{ item.event }}</span>
<span>{{ item.event ?? '-' }}</span>
</span>
</div>
</div>
@@ -210,7 +209,7 @@
import request from '~/utils/request.js'
import moment from 'moment'
import Message from '~/components/Message.vue'
import {formatDuration, makeSearchLink, notification} from '~/utils/index.js'
import {formatDuration, makeName, makeSearchLink, notification} from '~/utils/index.js'
const route = useRoute()
const router = useRouter()

View File

@@ -16,7 +16,7 @@
<i class="fas fa-eye-slash" v-if="!history.watched"></i>
<i class="fas fa-eye" v-else></i>
</span>
<NuxtLink :to="`/history/${history.id}`" v-text="history.full_title ?? history.title"/>
<NuxtLink :to="`/history/${history.id}`" v-text="makeName(history)"/>
</p>
<span class="card-header-icon">
<span class="icon" v-if="'episode' === history.type"><i class="fas fa-tv"></i></span>
@@ -26,24 +26,20 @@
<div class="card-content">
<div class="columns is-multiline is-mobile has-text-centered">
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
{{ moment(history.updated).fromNow() }}
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="moment.unix(history.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(history.updated).fromNow() }}
</span>
</div>
<div class="column is-4-tablet is-6-mobile has-text-right-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"></i></span>
<span>
<NuxtLink :to="'/backend/'+history.via" v-text="history.via"/>
</span>
</span>
<span class="icon"><i class="fas fa-server"></i>&nbsp;</span>
<NuxtLink :to="'/backend/'+history.via" v-text="history.via"/>
</div>
<div class="column is-4-tablet is-12-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-envelope"></i></span>
<span>{{ history.event }}</span>
</span>
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-envelope"></i>&nbsp;</span>
{{ history.event }}
</div>
</div>
</div>
</div>
@@ -112,7 +108,7 @@
import request from '~/utils/request.js'
import moment from 'moment'
import Message from '~/components/Message.vue'
import {formatDuration} from '../utils/index.js'
import {formatDuration, makeName} from '../utils/index.js'
useHead({title: 'Index'})

View File

@@ -76,7 +76,7 @@
<div class="card" :class="{ 'is-success': item.watched }">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<NuxtLink :to="'/history/'+item.id" v-text="item.full_title ?? item.title"/>
<NuxtLink :to="'/history/'+item.id" v-text="makeName(item)"/>
</p>
<span class="card-header-icon">
<span class="icon">
@@ -87,54 +87,46 @@
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12 " v-if="item?.title">
<div class="column is-12 " v-if="item?.content_title">
<div class="is-text-overflow is-clickable"
@click="(e) => e.target.classList.toggle('is-text-overflow')">
<span class="icon"><i class="fas fa-heading"></i></span>
<span class="is-hidden-mobile">Title:&nbsp;</span>
{{ item.title }}
<span class="icon"><i class="fas fa-heading"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle',item.content_title)" v-text="item.content_title"/>
</div>
</div>
<div class="column is-12 is-clickable " v-if="item?.path"
<div class="column is-12 is-clickable " v-if="item?.content_path"
@click="(e) => e.target.firstChild?.classList?.toggle('is-text-overflow')">
<div class="is-text-overflow">
<span class="icon"><i class="fas fa-file"></i></span>
<span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/>
<span class="icon"><i class="fas fa-file"></i>&nbsp;</span>
<NuxtLink :to="makeSearchLink('path',item.content_path)" v-text="item.content_path"/>
</div>
</div>
<div class="column is-12">
<span class="icon"><i class="fas fa-check"></i></span>
<span class="icon"><i class="fas fa-check"></i>&nbsp;</span>
<span v-for="backend in item.reported_by">
<NuxtLink :to="'/backend/'+backend" v-text="backend"
class="tag"/>
&nbsp;
<NuxtLink :to="'/backend/'+backend" v-text="backend" class="tag"/>
</span>
</div>
<div class="column is-12">
<span class="icon"><i class="fas fa-times"></i></span>
<span class="icon"><i class="fas fa-times"></i>&nbsp;</span>
<span v-for="backend in item.not_reported_by">
<NuxtLink :to="'/backend/'+backend" v-text="backend"
class="tag"/>
&nbsp;
<NuxtLink :to="'/backend/'+backend" v-text="backend" class="tag"/>
</span>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item">
<span class="icon-text">
<span class="icon">
<i class="fas" :class="{'fa-eye':item.watched,'fa-eye-slash':!item.watched}"></i>
</span>
<span class="has-text-success" v-if="item.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
<span class="icon">
<i class="fas" :class="{'fa-eye':item.watched,'fa-eye-slash':!item.watched}"></i>&nbsp;
</span>
<span class="has-text-success" v-if="item.watched">Played</span>
<span class="has-text-danger" v-else>Unplayed</span>
</div>
<div class="card-footer-item">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i></span>
<span>{{ moment(item.updated).fromNow() }}</span>
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="moment.unix(item.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(item.updated).fromNow() }}
</span>
</div>
</div>
@@ -179,7 +171,7 @@
<script setup>
import request from '~/utils/request.js'
import Message from '~/components/Message.vue'
import {makeSearchLink, notification} from '~/utils/index.js'
import {makeName, makeSearchLink, notification} from '~/utils/index.js'
import moment from 'moment'
import {useStorage} from '@vueuse/core'

View File

@@ -331,6 +331,34 @@ const dEvent = (eventName, detail = {}) => {
window.dispatchEvent(new CustomEvent(eventName, {detail}))
}
/**
* Make name
*
* @param item {Object}
* @param asMovie {boolean}
*
* @returns {string|null} The name of the item.
*/
const makeName = (item, asMovie = false) => {
if (!item) {
return null;
}
const year = ag(item, 'year', '0000');
const title = ag(item, 'title', '??');
const type = ag(item, 'type', 'movie');
if (['show', 'movie'].includes(type) || asMovie) {
return r('{title} ({year})', {title, year})
}
return r('{title} ({year}) - {season}x{episode}', {
title,
year,
season: ag(item, 'season', 0).toString().padStart(2, '0'),
episode: ag(item, 'episode', 0).toString().padStart(3, '0'),
})
}
export {
ag_set,
ag,
@@ -344,5 +372,6 @@ export {
stringToRegex,
makeConsoleCommand,
makeSearchLink,
dEvent
dEvent,
makeName,
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\API\History;
use App\Commands\Database\ListCommand;
use App\Libs\Attributes\Route\Delete;
use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Route;
@@ -22,6 +21,23 @@ use Psr\Http\Message\ServerRequestInterface as iRequest;
final class Index
{
/**
* @var array The array containing the names of the columns that the list can be sorted by.
*/
public const array COLUMNS_SORTABLE = [
iState::COLUMN_ID,
iState::COLUMN_TYPE,
iState::COLUMN_UPDATED,
iState::COLUMN_WATCHED,
iState::COLUMN_VIA,
iState::COLUMN_TITLE,
iState::COLUMN_YEAR,
iState::COLUMN_SEASON,
iState::COLUMN_EPISODE,
iState::COLUMN_CREATED_AT,
iState::COLUMN_UPDATED_AT,
];
use APITraits;
public const string URL = '%{api.prefix}/history';
@@ -279,7 +295,7 @@ final class Index
if (null === ($matches['field'] ?? null) || false === in_array(
$matches['field'],
ListCommand::COLUMNS_SORTABLE
self::COLUMNS_SORTABLE
)) {
continue;
}
@@ -295,7 +311,7 @@ final class Index
}
if (count($sorts) < 1) {
$sorts[] = sprintf('%s DESC', $es('updated'));
$sorts[] = sprintf('%s DESC', $es(iState::COLUMN_UPDATED_AT));
}
$params['_start'] = $start;
@@ -419,8 +435,8 @@ final class Index
],
[
'key' => 'subtitle',
'display' => 'Content title',
'description' => 'Search using content title. Searching this field will be slow.',
'display' => 'Subtitle',
'description' => 'Search using subtitle. Searching this field will be slow.',
'type' => 'string',
],
],
@@ -430,26 +446,29 @@ final class Index
$entity = Container::get(iState::class)->fromArray($row);
$item = $entity->getAll();
$item[iState::COLUMN_WATCHED] = $entity->isWatched();
$item[iState::COLUMN_UPDATED] = makeDate($entity->updated);
if (true === (bool)$request->getAttribute('INTERNAL_REQUEST')) {
$response['history'][] = $item;
continue;
}
if (!$data->get('metadata')) {
$item[iState::COLUMN_WATCHED] = $entity->isWatched();
if (!$data->get(iState::COLUMN_META_DATA)) {
unset($item[iState::COLUMN_META_DATA]);
}
if (!$data->get('extra')) {
if (!$data->get(iState::COLUMN_EXTRA)) {
unset($item[iState::COLUMN_EXTRA]);
}
$item['full_title'] = $entity->getName();
$item[iState::COLUMN_META_DATA_PROGRESS] = $entity->hasPlayProgress() ? $entity->getPlayProgress() : null;
$item[iState::COLUMN_EXTRA_EVENT] = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_EVENT, null);
$item[iState::COLUMN_TITLE] = $entity->isEpisode() ? ag(
$item['content_title'] = $entity->isEpisode() ? ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
null
) : null;
$item[iState::COLUMN_META_PATH] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$item['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$item['rguids'] = [];
@@ -479,10 +498,8 @@ final class Index
}
$r = $item->getAll();
$r['full_title'] = $item->getName();
$r[iState::COLUMN_META_DATA_PROGRESS] = $item->hasPlayProgress() ? $item->getPlayProgress() : null;
$r[iState::COLUMN_WATCHED] = $item->isWatched();
$r[iState::COLUMN_UPDATED] = makeDate($item->updated);
$r[iState::COLUMN_EXTRA_EVENT] = ag($item->getExtra($item->via), iState::COLUMN_EXTRA_EVENT, null);
$r['rguids'] = [];
@@ -508,15 +525,13 @@ final class Index
$backendsKeys = array_column($this->getBackends(), 'name');
$r['not_reported_by'] = array_values(
array_filter($backendsKeys, fn($key) => !in_array($key, $reportedBy))
);
$r[iState::COLUMN_TITLE] = $item->isEpisode() ? ag(
$r['not_reported_by'] = array_values(array_filter($backendsKeys, fn($key) => !in_array($key, $reportedBy)));
$r['content_title'] = $item->isEpisode() ? ag(
$item->getMetadata($item->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
null
) : null;
$r[iState::COLUMN_META_PATH] = ag($item->getMetadata($item->via), iState::COLUMN_META_PATH);
$r['content_path'] = ag($item->getMetadata($item->via), iState::COLUMN_META_PATH);
return api_response(HTTP_STATUS::HTTP_OK, $r);
}
@@ -573,13 +588,8 @@ final class Index
$this->mapper->add($item)->commit();
$response = $item->getAll();
$response['full_title'] = $item->getName();
$response[iState::COLUMN_WATCHED] = $item->isWatched();
$response[iState::COLUMN_UPDATED] = makeDate($item->updated);
queuePush($item);
return api_response(HTTP_STATUS::HTTP_OK, $response);
return $this->historyView($request, $args);
}
}

View File

@@ -123,25 +123,23 @@ final class Parity
$reportedBackends = array_keys($row[iState::COLUMN_META_DATA] ?? []);
$entity = Container::get(iState::class)->fromArray($row);
$response['items'][] = [
iState::COLUMN_ID => ag($row, 'id'),
iState::COLUMN_WATCHED => $entity->isWatched(),
iState::COLUMN_TYPE => ucfirst($entity->type),
iState::COLUMN_TITLE => $entity->isEpisode() ? ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
null
) : null,
'full_title' => $entity->getName(),
iState::COLUMN_UPDATED => makeDate($entity->updated),
'reported_by' => $reportedBackends,
'not_reported_by' => array_values(
array_filter($backendsKeys, fn($key) => !in_array($key, $reportedBackends))
),
iState::COLUMN_META_PATH => ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH),
];
}
$item = $entity->getAll();
$item[iState::COLUMN_WATCHED] = $entity->isWatched();
$item[iState::COLUMN_META_DATA_PROGRESS] = $entity->hasPlayProgress() ? $entity->getPlayProgress() : null;
$item[iState::COLUMN_EXTRA_EVENT] = ag($entity->getExtra($entity->via), iState::COLUMN_EXTRA_EVENT, null);
$item['content_title'] = $entity->isEpisode() ? ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
null
) : null;
$item['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$item['reported_by'] = $reportedBackends;
$item['not_reported_by'] = array_values(
array_filter($backendsKeys, fn($key) => !in_array($key, $reportedBackends))
);
$response['items'][] = $item;
}
return api_response(HTTP_STATUS::HTTP_OK, $response);
}

View File

@@ -90,13 +90,12 @@ class SearchId
)
);
$builder[iState::COLUMN_TITLE] = ag(
$builder['content_title'] = ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
$entity->title
);
$builder['full_title'] = $entity->getName();
$builder[iState::COLUMN_META_PATH] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$builder['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
if (true === (bool)ag($opts, Options::RAW_RESPONSE)) {
$builder['raw'] = $item;

View File

@@ -160,13 +160,12 @@ class SearchQuery
)
);
$builder[iState::COLUMN_TITLE] = ag(
$builder['content_title'] = ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
$entity->title
);
$builder['full_title'] = $entity->getName();
$builder[iState::COLUMN_META_PATH] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$builder['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
if (true === (bool)ag($opts, Options::RAW_RESPONSE)) {
$builder[Options::RAW_RESPONSE] = $item;

View File

@@ -82,13 +82,12 @@ final class SearchId
)
);
$builder[iState::COLUMN_TITLE] = ag(
$builder['content_title'] = ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
$entity->title
);
$builder['full_title'] = $entity->getName();
$builder[iState::COLUMN_META_PATH] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$builder['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
if (true === (bool)ag($opts, Options::RAW_RESPONSE)) {
$builder['raw'] = $item;

View File

@@ -157,13 +157,12 @@ final class SearchQuery
)
);
$builder[iState::COLUMN_TITLE] = ag(
$builder['content_title'] = ag(
$entity->getMetadata($entity->via),
iState::COLUMN_EXTRA . '.' . iState::COLUMN_TITLE,
$entity->title
);
$builder['full_title'] = $entity->getName();
$builder[iState::COLUMN_META_PATH] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
$builder['content_path'] = ag($entity->getMetadata($entity->via), iState::COLUMN_META_PATH);
if (true === (bool)ag($opts, Options::RAW_RESPONSE)) {
$builder[Options::RAW_RESPONSE] = $item;

View File

@@ -4,23 +4,22 @@ declare(strict_types=1);
namespace App\Commands\Database;
use App\API\History\Index;
use App\Command;
use App\Libs\Attributes\Route\Cli;
use App\Libs\Config;
use App\Libs\Container;
use App\Libs\Database\DatabaseInterface as iDB;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\Exceptions\RuntimeException;
use App\Libs\Guid;
use App\Libs\HTTP_STATUS;
use App\Libs\Mappers\Import\DirectMapper;
use PDO;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
/**
* Class ListCommand
@@ -32,48 +31,15 @@ final class ListCommand extends Command
{
public const string ROUTE = 'db:list';
/**
* @var array The array containing the names of the columns that can be modified for viewing purposes.
*/
public const array COLUMNS_CHANGEABLE = [
iState::COLUMN_WATCHED,
iState::COLUMN_VIA,
iState::COLUMN_TITLE,
iState::COLUMN_YEAR,
iState::COLUMN_SEASON,
iState::COLUMN_EPISODE,
iState::COLUMN_UPDATED,
];
/**
* @var array The array containing the names of the columns that the list can be sorted by.
*/
public const array COLUMNS_SORTABLE = [
iState::COLUMN_ID,
iState::COLUMN_TYPE,
iState::COLUMN_UPDATED,
iState::COLUMN_WATCHED,
iState::COLUMN_VIA,
iState::COLUMN_TITLE,
iState::COLUMN_YEAR,
iState::COLUMN_SEASON,
iState::COLUMN_EPISODE,
];
private PDO $pdo;
/**
* Class constructor.
*
* @param iDB $db The database object.
* @param DirectMapper $mapper The direct mapper object.
*
* @return void
*/
public function __construct(private iDB $db, private DirectMapper $mapper)
public function __construct(private DirectMapper $mapper)
{
$this->pdo = $this->db->getPDO();
parent::__construct();
}
@@ -106,6 +72,8 @@ final class ListCommand extends Command
'Limit results to this specified type can be [movie or episode].'
)
->addOption('title', null, InputOption::VALUE_REQUIRED, 'Limit results to this specified title.')
->addOption('subtitle', null, InputOption::VALUE_REQUIRED, 'Limit results to this specified content title.')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'Show results that contains this file path.')
->addOption('season', null, InputOption::VALUE_REQUIRED, 'Select season number.')
->addOption('episode', null, InputOption::VALUE_REQUIRED, 'Select episode number.')
->addOption('year', null, InputOption::VALUE_REQUIRED, 'Select year.')
@@ -113,14 +81,8 @@ final class ListCommand extends Command
->addOption(
'sort',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Set sort by columns. [Example: <flag>--sort</flag> <value>season:asc</value>].',
)
->addOption(
'show-as',
null,
InputOption::VALUE_REQUIRED,
'Switch the default metadata display to the chosen backend instead of latest metadata.'
'Set sort by columns. [Example: <flag>--sort</flag> <value>season:asc</value>].',
)
->addOption(
'guid',
@@ -148,12 +110,6 @@ final class ListCommand extends Command
InputOption::VALUE_NONE,
'Search in (<notice>extra</notice>) info by backends JSON field. Expects [<flag>--key</flag>, <flag>--value</flag>] flags.'
)
->addOption(
'dump-query',
null,
InputOption::VALUE_NONE,
'Dump the generated query and exit.'
)
->addOption(
'exact',
null,
@@ -228,37 +184,23 @@ final class ListCommand extends Command
{
$limit = (int)$input->getOption('limit');
if (null !== ($changeState = $input->getOption('mark-as'))) {
$limit = PHP_INT_MAX;
}
$es = fn(string $val) => $this->db->identifier($val);
$params = [
'limit' => $limit <= 0 ? 20 : $limit,
'perpage' => $limit <= 0 ? 20 : $limit,
];
$sql = $where = [];
$sql[] = sprintf('SELECT * FROM %s', $es('state'));
if ($input->getOption('id')) {
$where[] = $es(iState::COLUMN_ID) . ' = :id';
$params['id'] = $input->getOption('id');
}
if ($input->getOption('via')) {
$where[] = $es(iState::COLUMN_VIA) . ' = :via';
$params['via'] = $input->getOption('via');
}
if ($input->getOption('year')) {
$where[] = $es(iState::COLUMN_YEAR) . ' = :year';
$params['year'] = $input->getOption('year');
}
if ($input->getOption('type')) {
$where[] = $es(iState::COLUMN_TYPE) . ' = :type';
$params['type'] = match ($input->getOption('type')) {
iState::TYPE_MOVIE => iState::TYPE_MOVIE,
default => iState::TYPE_EPISODE,
@@ -266,17 +208,18 @@ final class ListCommand extends Command
}
if ($input->getOption('title')) {
$where[] = $es(iState::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
$params['title'] = $input->getOption('title');
}
if ($input->getOption('subtitle')) {
$params['subtitle'] = $input->getOption('subtitle');
}
if (null !== $input->getOption('season')) {
$where[] = $es(iState::COLUMN_SEASON) . ' = :season';
$params['season'] = $input->getOption('season');
}
if (null !== $input->getOption('episode')) {
$where[] = $es(iState::COLUMN_EPISODE) . ' = :episode';
$params['episode'] = $input->getOption('episode');
}
@@ -291,7 +234,6 @@ final class ListCommand extends Command
return self::INVALID;
}
$where[] = "json_extract(" . iState::COLUMN_PARENT . ",'$.{$parent}') = :parent";
$params['parent'] = array_values($d->getAll())[0];
}
@@ -306,7 +248,6 @@ final class ListCommand extends Command
return self::INVALID;
}
$where[] = "json_extract(" . iState::COLUMN_GUIDS . ",'$.{$guid}') = :guid";
$params['guid'] = array_values($d->getAll())[0];
}
@@ -319,13 +260,10 @@ final class ListCommand extends Command
);
}
if ($input->getOption('exact')) {
$where[] = "json_extract(" . iState::COLUMN_META_DATA . ",'$.{$sField}') = :jf_metadata_value ";
} else {
$where[] = "json_extract(" . iState::COLUMN_META_DATA . ",'$.{$sField}') LIKE \"%\" || :jf_metadata_value || \"%\"";
}
$params['jf_metadata_value'] = $sValue;
$params['exact'] = (int)$input->getOption('exact');
$params[iState::COLUMN_META_DATA] = 1;
$params['key'] = $sField;
$params['value'] = $sValue;
}
if ($input->getOption('extra')) {
@@ -337,143 +275,54 @@ final class ListCommand extends Command
);
}
if ($input->getOption('exact')) {
$where[] = "json_extract(" . iState::COLUMN_EXTRA . ",'$.{$sField}') = :jf_extra_value";
} else {
$where[] = "json_extract(" . iState::COLUMN_EXTRA . ",'$.{$sField}') LIKE \"%\" || :jf_extra_value || \"%\"";
}
$params['jf_extra_value'] = $sValue;
$params['exact'] = (int)$input->getOption('exact');
$params[iState::COLUMN_EXTRA] = 1;
$params['key'] = $sField;
$params['value'] = $sValue;
}
if (count($where) >= 1) {
$sql[] = 'WHERE ' . implode(' AND ', $where);
}
$sorts = [];
foreach ($input->getOption('sort') as $sort) {
if (null !== ($sort = $input->getOption('sort'))) {
if (1 !== preg_match('/(?P<field>\w+)(:(?P<dir>\w+))?/', $sort, $matches)) {
continue;
$output->writeln(
'<error>ERROR:</error> Invalid value for [<flag>--sort</flag>] expected value format is [<value>field:dir</value>].'
);
return self::INVALID;
}
if (null === ($matches['field'] ?? null) || false === in_array($matches['field'], self::COLUMNS_SORTABLE)) {
continue;
}
$sorts[] = sprintf(
'%s %s',
$es($matches['field']),
match (strtolower($matches['dir'] ?? 'desc')) {
default => 'DESC',
'asc' => 'ASC',
}
);
$params['sort'] = r('{field}:{dir}', $matches);
}
if (count($sorts) < 1) {
$sorts[] = sprintf('%s DESC', $es('updated'));
}
$sql[] = 'ORDER BY ' . implode(', ', $sorts) . ' LIMIT :limit';
$sql = implode(' ', array_map('trim', $sql));
if ($input->getOption('dump-query')) {
$arr = [
'query' => $sql,
'parameters' => $params,
];
if ('table' === $input->getOption('output')) {
$arr = [$arr];
$arr[0]['parameters'] = arrayToString($params);
$arr[1] = [
'query' => $this->db->getRawSQLString($sql, $params),
'parameters' => 'raw sql query',
];
} else {
$arr['raw'] = $this->db->getRawSQLString($sql, $params);
}
$this->displayContent($arr, $output, $input->getOption('output'));
return self::SUCCESS;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$rowCount = count($rows);
if (0 === $rowCount) {
$arr = [
'Error' => 'No Results.',
'Filters' => $params
];
if (true === ($hasFilters = count($arr['Filters']) > 1)) {
$arr['Error'] .= ' Probably invalid filters values were used.';
}
if ($hasFilters && 'table' !== $input->getOption('output')) {
array_shift($arr['Filters']);
if ('json' === $input->getOption('output')) {
$output->writeln(
json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
} elseif ('yaml' === $input->getOption('output')) {
$output->writeln(Yaml::dump($arr, 8, 2));
}
} else {
$output->writeln('<error>' . $arr['Error'] . '</error>');
}
$response = APIRequest('GET', '/history', [
'query' => $params,
]);
if (HTTP_STATUS::HTTP_OK !== $response->status) {
$output->writeln(r("<error>API error. {status}: {message}</error>", [
'status' => $response->status->value,
'message' => ag($response->body, 'error.message', 'Unknown error.')
]));
return self::FAILURE;
}
foreach ($rows as &$row) {
foreach (iState::ENTITY_ARRAY_KEYS as $key) {
if (null === ($row[$key] ?? null)) {
continue;
}
$row[$key] = json_decode($row[$key], true);
}
$rows = ag($response->body, 'history', []);
if (null !== ($via = $input->getOption('show-as'))) {
$path = $row[iState::COLUMN_META_DATA][$via] ?? [];
foreach (self::COLUMNS_CHANGEABLE as $column) {
if (null === ($path[$column] ?? null)) {
continue;
}
$row[$column] = 'int' === get_debug_type($row[$column]) ? (int)$path[$column] : $path[$column];
}
if (null !== ($dateFromBackend = $path[iState::COLUMN_META_DATA_PLAYED_AT] ?? $path[iState::COLUMN_META_DATA_ADDED_AT] ?? null)) {
$row[iState::COLUMN_UPDATED] = $dateFromBackend;
}
}
$row[iState::COLUMN_WATCHED] = (bool)$row[iState::COLUMN_WATCHED];
$row[iState::COLUMN_UPDATED] = makeDate($row[iState::COLUMN_UPDATED]);
if (empty($rows)) {
$output->writeln('<info>No results found.</info>');
return self::SUCCESS;
}
unset($row);
if ('table' === $input->getOption('output')) {
foreach ($rows as &$row) {
$row[iState::COLUMN_UPDATED] = $row[iState::COLUMN_UPDATED]->getTimestamp();
$row[iState::COLUMN_UPDATED] = makeDate($row[iState::COLUMN_UPDATED])->getTimestamp();
$row[iState::COLUMN_UPDATED_AT] = makeDate($row[iState::COLUMN_UPDATED_AT])->getTimestamp();
$row[iState::COLUMN_CREATED_AT] = makeDate($row[iState::COLUMN_CREATED_AT])->getTimestamp();
$row[iState::COLUMN_WATCHED] = (int)$row[iState::COLUMN_WATCHED];
$entity = Container::get(iState::class)->fromArray($row);
$row = [
'id' => $entity->id,
'type' => ucfirst($entity->type),
'title' => $entity->getName(),
'title' => mb_substr($entity->getName(), 0, 59),
'via' => $entity->via ?? '??',
'date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'),
'played' => $entity->isWatched() ? 'Yes' : 'No',
@@ -486,8 +335,9 @@ final class ListCommand extends Command
$this->displayContent($rows, $output, $input->getOption('output'));
if (null !== $changeState && count($rows) >= 1) {
if (null !== ($changeState = $input->getOption('mark-as')) && count($rows) >= 1) {
$changeState = strtolower($changeState);
if (!$input->getOption('no-interaction')) {
$text = r(
'<question>Are you sure you want to mark [<notice>{total}</notice>] items as [<notice>{state}</notice>]</question> ? [<value>Y|N</value>] [<value>Default: No</value>]',
@@ -591,7 +441,7 @@ final class ListCommand extends Command
$suggest = [];
foreach (self::COLUMNS_SORTABLE as $name) {
foreach (Index::COLUMNS_SORTABLE as $name) {
foreach ([$name . ':desc', $name . ':asc'] as $subName) {
if (empty($currentValue) || true === str_starts_with($subName, $currentValue)) {
$suggest[] = $subName;

View File

@@ -8,6 +8,7 @@ use App\Command;
use App\Libs\Attributes\Route\Cli;
use App\Libs\Config;
use App\Libs\ConfigFile;
use App\Libs\Container;
use App\Libs\Entity\StateInterface as iState;
use App\Libs\HTTP_STATUS;
use Symfony\Component\Console\Input\InputInterface;
@@ -193,12 +194,17 @@ final class ParityCommand extends Command
if ('table' === $input->getOption('output')) {
foreach ($rows as &$row) {
$played = ag($row, iState::COLUMN_WATCHED) ? '' : '';
$row['Reported by'] = join(', ', ag($row, 'reported_by', []));
$row['Not reported by'] = join(', ', ag($row, 'not_reported_by', []));
$row[iState::COLUMN_TITLE] = $played . ' ' . $row['full_title'];
unset($row[iState::COLUMN_WATCHED], $row['full_title'], $row['reported_by'], $row['not_reported_by']);
$row[iState::COLUMN_UPDATED] = makeDate($row[iState::COLUMN_UPDATED])->format('Y-m-d');
$row[iState::COLUMN_WATCHED] = (int)$row[iState::COLUMN_WATCHED];
$entity = Container::get(iState::class)->fromArray($row);
$played = $entity->isWatched() ? '' : '';
$item = [
iState::COLUMN_ID => $entity->id,
iState::COLUMN_TITLE => $played . ' ' . $entity->getName(),
istate::COLUMN_UPDATED => makeDate($row[iState::COLUMN_UPDATED])->format('Y-m-d'),
'Reported by' => join(', ', ag($row, 'reported_by', [])),
'Not reported by' => join(', ', ag($row, 'not_reported_by', [])),
];
$row = $item;
}
}

View File

@@ -25,6 +25,12 @@ final class StateEntity implements iState
* @var array $data Holds the original entity data.
*/
private array $data = [];
/**
* @var array $context Holds the context data for the entity.
*/
private array $context = [];
/**
* @var bool $tainted Flag indicating if the data is tainted based on its event type.
*/
@@ -93,6 +99,10 @@ final class StateEntity implements iState
*/
public array $extra = [];
public int $created_at = 0;
public int $updated_at = 0;
/**
* Constructor for the StateEntity class
*
@@ -220,6 +230,8 @@ final class StateEntity implements iState
iState::COLUMN_GUIDS => $this->guids,
iState::COLUMN_META_DATA => $this->metadata,
iState::COLUMN_EXTRA => $this->extra,
iState::COLUMN_CREATED_AT => $this->created_at,
iState::COLUMN_UPDATED_AT => $this->updated_at,
];
}
@@ -613,6 +625,27 @@ final class StateEntity implements iState
return $lastProgress;
}
/**
* @inheritdoc
*/
public function setContext(string $key, mixed $value): iState
{
$this->context = ag_set($this->context, $key, $value);
return $this;
}
/**
* @inheritdoc
*/
public function getContext(string|null $key = null, mixed $default = null): mixed
{
if (null === $key) {
return $default ?? $this->context;
}
return ag($this->context, $key, $default);
}
/**
* Checks if the value of a given key in the entity object is equal to the corresponding value in the current object.
* Some keys are special and require special logic to compare. For example, the updated and watched keys are special
@@ -710,5 +743,4 @@ final class StateEntity implements iState
return $difference;
}
}

View File

@@ -50,6 +50,9 @@ interface StateInterface extends LoggerAwareInterface
public const string COLUMN_EXTRA_EVENT = 'event';
public const string COLUMN_EXTRA_DATE = 'received_at';
public const string COLUMN_CREATED_AT = 'created_at';
public const string COLUMN_UPDATED_AT = 'updated_at';
/**
* List of table keys.
*/
@@ -67,6 +70,8 @@ interface StateInterface extends LoggerAwareInterface
self::COLUMN_GUIDS,
self::COLUMN_META_DATA,
self::COLUMN_EXTRA,
self::COLUMN_CREATED_AT,
self::COLUMN_UPDATED_AT,
];
/**
@@ -79,6 +84,8 @@ interface StateInterface extends LoggerAwareInterface
self::COLUMN_SEASON,
self::COLUMN_EPISODE,
self::COLUMN_EXTRA,
self::COLUMN_CREATED_AT,
self::COLUMN_UPDATED_AT,
];
/**
@@ -374,4 +381,24 @@ interface StateInterface extends LoggerAwareInterface
* @return int Return the play progress.
*/
public function getPlayProgress(): int;
/**
* Set entity contextual data.
*
* @param string $key key
* @param mixed $value value
*
* @return StateInterface Returns the current object.
*/
public function setContext(string $key, mixed $value): StateInterface;
/**
* Get entity contextual data.
*
* @param string|null $key the key to get, if both key and default are null, the entire context is returned.
* @param mixed $default default value.
*
* @return mixed
*/
public function getContext(string|null $key = null, mixed $default = null): mixed;
}

View File

@@ -50,4 +50,6 @@ return [
iState::COLUMN_EXTRA_EVENT => 'media.scrobble'
],
],
iState::COLUMN_CREATED_AT => 2,
iState::COLUMN_UPDATED_AT => 2,
];

View File

@@ -44,4 +44,6 @@ return [
iState::COLUMN_EXTRA_DATE => 2,
],
],
iState::COLUMN_CREATED_AT => 2,
iState::COLUMN_UPDATED_AT => 2,
];

View File

@@ -857,4 +857,32 @@ class StateEntityTest extends TestCase
'When hasPlayProgress() when valid play progress is set, returns true'
);
}
public function test_context(): void
{
$entity = new StateEntity($this->testMovie);
$ins = $entity->setContext('test', 'context');
$this->assertSame($ins, $entity, 'When setContext() is called, it returns the same instance');
$this->assertSame(
'context',
$entity->getContext('test'),
'When getContext() is called, the same value is returned'
);
$this->assertSame(
'iam_default',
$entity->getContext(null, 'iam_default'),
'When getContext() is called with default value, and key is null the default value is returned'
);
$this->assertSame(
'iam_default',
$entity->getContext('not_set', 'iam_default'),
'When getContext() is called with non-existing key, the default value is returned'
);
$this->assertSame(
['test' => 'context'],
$entity->getContext(),
'When getContext() is called with no parameters, all context data is returned'
);
}
}