Standardizing how we report the items via API, there are more work to be done.
This commit is contained in:
@@ -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> </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()
|
||||
|
||||
@@ -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: </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: </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> </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";
|
||||
|
||||
|
||||
@@ -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: </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: </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: </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: </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: </span>
|
||||
<NuxtLink :to="makeSearchLink('subtitle', data.title)" v-text="data.title"/>
|
||||
<span class="is-hidden-mobile">Subtitle: </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: </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: </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: </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: </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: </span>
|
||||
<span class="is-hidden-mobile">Subtitle: </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>
|
||||
|
||||
@@ -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: </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: </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> </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()
|
||||
|
||||
@@ -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> </span>
|
||||
{{ moment(history.updated).fromNow() }}
|
||||
<span class="icon"><i class="fas fa-calendar"></i> </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> </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> </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'})
|
||||
|
||||
|
||||
@@ -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: </span>
|
||||
{{ item.title }}
|
||||
<span class="icon"><i class="fas fa-heading"></i> </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: </span>
|
||||
<NuxtLink :to="makeSearchLink('path',item.path)" v-text="item.path"/>
|
||||
<span class="icon"><i class="fas fa-file"></i> </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> </span>
|
||||
<span v-for="backend in item.reported_by">
|
||||
<NuxtLink :to="'/backend/'+backend" v-text="backend"
|
||||
class="tag"/>
|
||||
|
||||
<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> </span>
|
||||
<span v-for="backend in item.not_reported_by">
|
||||
<NuxtLink :to="'/backend/'+backend" v-text="backend"
|
||||
class="tag"/>
|
||||
|
||||
<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>
|
||||
</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> </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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -50,4 +50,6 @@ return [
|
||||
iState::COLUMN_EXTRA_EVENT => 'media.scrobble'
|
||||
],
|
||||
],
|
||||
iState::COLUMN_CREATED_AT => 2,
|
||||
iState::COLUMN_UPDATED_AT => 2,
|
||||
];
|
||||
|
||||
@@ -44,4 +44,6 @@ return [
|
||||
iState::COLUMN_EXTRA_DATE => 2,
|
||||
],
|
||||
],
|
||||
iState::COLUMN_CREATED_AT => 2,
|
||||
iState::COLUMN_UPDATED_AT => 2,
|
||||
];
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user