Added metadata status to item record view.

This commit is contained in:
arabcoders
2025-05-09 18:56:02 +03:00
parent 0ff127875f
commit 689716e366
3 changed files with 295 additions and 127 deletions

25
composer.lock generated
View File

@@ -3588,12 +3588,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git", "url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "45b01f4e60c350f72a8697056674e449e053935a" "reference": "59be420c5cdc0c3c9cdae31804d32fefb515a918"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/45b01f4e60c350f72a8697056674e449e053935a", "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/59be420c5cdc0c3c9cdae31804d32fefb515a918",
"reference": "45b01f4e60c350f72a8697056674e449e053935a", "reference": "59be420c5cdc0c3c9cdae31804d32fefb515a918",
"shasum": "" "shasum": ""
}, },
"conflict": { "conflict": {
@@ -3611,7 +3611,7 @@
"airesvsg/acf-to-rest-api": "<=3.1", "airesvsg/acf-to-rest-api": "<=3.1",
"akaunting/akaunting": "<2.1.13", "akaunting/akaunting": "<2.1.13",
"akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
"alextselegidis/easyappointments": "<=1.5", "alextselegidis/easyappointments": "<=1.5.1",
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
"amazing/media2click": ">=1,<1.3.3", "amazing/media2click": ">=1,<1.3.3",
"ameos/ameos_tarteaucitron": "<1.2.23", "ameos/ameos_tarteaucitron": "<1.2.23",
@@ -3715,7 +3715,7 @@
"contao/managed-edition": "<=1.5", "contao/managed-edition": "<=1.5",
"corveda/phpsandbox": "<1.3.5", "corveda/phpsandbox": "<1.3.5",
"cosenary/instagram": "<=2.3", "cosenary/instagram": "<=2.3",
"craftcms/cms": "<=4.14.14|>=5,<=5.6.16", "craftcms/cms": "<4.15.3|>=5,<5.7.5",
"croogo/croogo": "<4", "croogo/croogo": "<4",
"cuyz/valinor": "<0.12", "cuyz/valinor": "<0.12",
"czim/file-handling": "<1.5|>=2,<2.3", "czim/file-handling": "<1.5|>=2,<2.3",
@@ -3948,6 +3948,7 @@
"klaviyo/magento2-extension": ">=1,<3", "klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.2", "knplabs/knp-snappy": "<=1.4.2",
"kohana/core": "<3.3.3", "kohana/core": "<3.3.3",
"koillection/koillection": "<1.6.12",
"krayin/laravel-crm": "<=1.3", "krayin/laravel-crm": "<=1.3",
"kreait/firebase-php": ">=3.2,<3.8.1", "kreait/firebase-php": ">=3.2,<3.8.1",
"kumbiaphp/kumbiapp": "<=1.1.1", "kumbiaphp/kumbiapp": "<=1.1.1",
@@ -3966,7 +3967,7 @@
"latte/latte": "<2.10.8", "latte/latte": "<2.10.8",
"lavalite/cms": "<=9|==10.1", "lavalite/cms": "<=9|==10.1",
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
"league/commonmark": "<2.6", "league/commonmark": "<2.7",
"league/flysystem": "<1.1.4|>=2,<2.1.1", "league/flysystem": "<1.1.4|>=2,<2.1.1",
"league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3",
"leantime/leantime": "<3.3", "leantime/leantime": "<3.3",
@@ -4066,9 +4067,9 @@
"nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
"october/backend": "<1.1.2", "october/backend": "<1.1.2",
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
"october/october": "<=3.6.4", "october/october": "<3.7.5",
"october/rain": "<1.0.472|>=1.1,<1.1.2", "october/rain": "<1.0.472|>=1.1,<1.1.2",
"october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.15", "october/system": "<3.7.5",
"oliverklee/phpunit": "<3.5.15", "oliverklee/phpunit": "<3.5.15",
"omeka/omeka-s": "<4.0.3", "omeka/omeka-s": "<4.0.3",
"onelogin/php-saml": "<2.10.4", "onelogin/php-saml": "<2.10.4",
@@ -4196,8 +4197,8 @@
"serluck/phpwhois": "<=4.2.6", "serluck/phpwhois": "<=4.2.6",
"sfroemken/url_redirect": "<=1.2.1", "sfroemken/url_redirect": "<=1.2.1",
"sheng/yiicms": "<1.2.1", "sheng/yiicms": "<1.2.1",
"shopware/core": "<6.5.8.17-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", "shopware/core": "<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev",
"shopware/platform": "<6.5.8.17-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", "shopware/platform": "<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev",
"shopware/production": "<=6.3.5.2", "shopware/production": "<=6.3.5.2",
"shopware/shopware": "<=5.7.17", "shopware/shopware": "<=5.7.17",
"shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev",
@@ -4240,7 +4241,7 @@
"slim/slim": "<2.6", "slim/slim": "<2.6",
"slub/slub-events": "<3.0.3", "slub/slub-events": "<3.0.3",
"smarty/smarty": "<4.5.3|>=5,<5.1.1", "smarty/smarty": "<4.5.3|>=5,<5.1.1",
"snipe/snipe-it": "<=7.0.13", "snipe/snipe-it": "<8.1",
"socalnick/scn-social-auth": "<1.15.2", "socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1", "socialiteproviders/steam": "<1.1",
"spatie/browsershot": "<5.0.5", "spatie/browsershot": "<5.0.5",
@@ -4510,7 +4511,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-05-01T20:05:59+00:00" "time": "2025-05-08T20:06:04+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View File

@@ -4,7 +4,7 @@
<div class="column is-12 is-clearfix"> <div class="column is-12 is-clearfix">
<span class="title is-4"> <span class="title is-4">
<span class="is-unselectable"> <span class="is-unselectable">
<span class="icon"><i class="fas fa-history" />&nbsp;</span> <span class="icon"><i class="fas fa-history"/>&nbsp;</span>
<NuxtLink to="/history">History</NuxtLink> <NuxtLink to="/history">History</NuxtLink>
: :
</span>{{ headerTitle }} </span>{{ headerTitle }}
@@ -13,35 +13,35 @@
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<button @click="api_show_photos = !api_show_photos" class="button is-purple" <button @click="api_show_photos = !api_show_photos" class="button is-purple"
v-tooltip.bottom="`${api_show_photos ? 'Hide' : 'Show'} fanart`"> v-tooltip.bottom="`${api_show_photos ? 'Hide' : 'Show'} fanart`">
<span class="icon"><i class="fas fa-image" /></span> <span class="icon"><i class="fas fa-image"/></span>
</button> </button>
</p> </p>
<p class="control" v-if="data?.files?.length > 0"> <p class="control" v-if="data?.files?.length > 0">
<button @click="navigateTo(`/play/${data.id}`)" class="button has-text-white has-background-danger-50" <button @click="navigateTo(`/play/${data.id}`)" class="button has-text-white has-background-danger-50"
v-tooltip.bottom="`${data.content_exists ? 'Play media' : 'Media is inaccessible'}`" v-tooltip.bottom="`${data.content_exists ? 'Play media' : 'Media is inaccessible'}`"
:disabled="!data.content_exists"> :disabled="!data.content_exists">
<span class="icon"><i class="fas fa-play" /></span> <span class="icon"><i class="fas fa-play"/></span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button" @click="toggleWatched" <button class="button" @click="toggleWatched"
:class="{ 'is-success': !data.watched, 'is-danger': data.watched }" :class="{ 'is-success': !data.watched, 'is-danger': data.watched }"
v-tooltip.bottom="'Toggle watch state'"> v-tooltip.bottom="'Toggle watch state'">
<span class="icon"> <span class="icon">
<i class="fas" :class="{ 'fa-eye-slash': data.watched, 'fa-eye': !data.watched }" /> <i class="fas" :class="{ 'fa-eye-slash': data.watched, 'fa-eye': !data.watched }"/>
</span> </span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-danger" @click="deleteItem(data)" v-tooltip.bottom="'Delete the record'" <button class="button is-danger" @click="deleteItem(data)" v-tooltip.bottom="'Delete the record'"
:disabled="isDeleting || isLoading" :class="{ 'is-loading': isDeleting }"> :disabled="isDeleting || isLoading" :class="{ 'is-loading': isDeleting }">
<span class="icon"><i class="fas fa-trash" /></span> <span class="icon"><i class="fas fa-trash"/></span>
</button> </button>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-info" @click="loadContent(id)" :class="{ 'is-loading': isLoading }"> <button class="button is-info" @click="loadContent(id)" :class="{ 'is-loading': isLoading }">
<span class="icon"><i class="fas fa-sync" /></span> <span class="icon"><i class="fas fa-sync"/></span>
</button> </button>
</p> </p>
</div> </div>
@@ -49,23 +49,23 @@
<div class="subtitle is-5" v-if="data?.via && (data?.content_title || data?.content_overview)"> <div class="subtitle is-5" v-if="data?.via && (data?.content_title || data?.content_overview)">
<template v-if="data?.content_title"> <template v-if="data?.content_title">
<span class="is-unselectable icon"> <span class="is-unselectable icon">
<i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }" /> <i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }"/>
</span> </span>
{{ data?.content_title }} {{ data?.content_title }}
</template> </template>
<div v-if="data?.content_overview" class="is-hidden-mobile is-clickable" <div v-if="data?.content_overview" class="is-hidden-mobile is-clickable"
@click="expandOverview = !expandOverview" :class="{ 'is-text-overflow': !expandOverview }"> @click="expandOverview = !expandOverview" :class="{ 'is-text-overflow': !expandOverview }">
<span class="is-unselectable icon" v-if="!data?.content_title"> <span class="is-unselectable icon" v-if="!data?.content_title">
<i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }" /> <i class="fas fa-tv" :class="{ 'fa-tv': 'episode' === data.type, 'fa-film': 'movie' === data.type }"/>
</span> </span>
{{ expandOverview ? data.content_overview : data.content_overview }} {{ expandOverview ? data.content_overview : data.content_overview }}
</div> </div>
<div v-if="data?.content_genres && data.content_genres.length > 0" class="is-hidden-mobile is-clickable" <div v-if="data?.content_genres && data.content_genres.length > 0" class="is-hidden-mobile is-clickable"
:class="{ 'is-text-overflow': !expandGenres }" @click="expandGenres = !expandGenres"> :class="{ 'is-text-overflow': !expandGenres }" @click="expandGenres = !expandGenres">
<span class="tag is-info is-clickable mr-1" v-for="(genre, id) in data.content_genres" <span class="tag is-info is-clickable mr-1" v-for="(genre, id) in data.content_genres"
:key="`head-genre-${id}`"> :key="`head-genre-${id}`">
<span class="icon"><i class="fas fa-tag" /></span> <span class="icon"><i class="fas fa-tag"/></span>
<span class="is-capitalized" v-text="genre" /> <span class="is-capitalized" v-text="genre"/>
</span> </span>
</div> </div>
</div> </div>
@@ -73,23 +73,23 @@
<div class="column is-12" v-if="!data?.via && isLoading"> <div class="column is-12" v-if="!data?.via && isLoading">
<Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin" <Message message_class="has-background-info-90 has-text-dark" title="Loading" icon="fas fa-spinner fa-spin"
message="Loading data. Please wait..." /> message="Loading data. Please wait..."/>
</div> </div>
<div class="column is-12" v-if="data?.not_reported_by && data.not_reported_by.length > 0"> <div class="column is-12" v-if="data?.not_reported_by && data.not_reported_by.length > 0">
<Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle" <Message message_class="has-background-warning-80 has-text-dark" icon="fas fa-exclamation-triangle"
:toggle="show_history_page_warning" title="Warning" :use-toggle="true" :toggle="show_history_page_warning" title="Warning" :use-toggle="true"
@toggle="show_history_page_warning = !show_history_page_warning"> @toggle="show_history_page_warning = !show_history_page_warning">
<p> <p>
<span class="icon"><i class="fas fa-exclamation" /></span> <span class="icon"><i class="fas fa-exclamation"/></span>
There are no metadata regarding this <strong>{{ data.type }}</strong> from ( There are no metadata regarding this <strong>{{ data.type }}</strong> from (
<span class="tag mr-1 has-text-dark" v-for="backend in data.not_reported_by" :key="`nr-${backend}`"> <span class="tag mr-1 has-text-dark" v-for="backend in data.not_reported_by" :key="`nr-${backend}`">
<NuxtLink :to="`/backend/${backend}`" v-text="backend" /> <NuxtLink :to="`/backend/${backend}`" v-text="backend"/>
</span>). </span>).
</p> </p>
<h5 class="has-text-dark"> <h5 class="has-text-dark">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-question-circle" /></span> <span class="icon"><i class="fas fa-question-circle"/></span>
<span>Possible reasons</span> <span>Possible reasons</span>
</span> </span>
</h5> </h5>
@@ -113,15 +113,15 @@
<header class="card-header"> <header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="data._toggle = !data._toggle"> <div class="card-header-title is-clickable is-unselectable" @click="data._toggle = !data._toggle">
<span class="icon"> <span class="icon">
<i class="fas" :class="{ 'fa-arrow-up': data?._toggle, 'fa-arrow-down': !data?._toggle }" /> <i class="fas" :class="{ 'fa-arrow-up': data?._toggle, 'fa-arrow-down': !data?._toggle }"/>
</span> </span>
<span>Latest local metadata via</span> <span>Latest local metadata via</span>
</div> </div>
<div class="card-header-icon"> <div class="card-header-icon">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-server" /></span> <span class="icon"><i class="fas fa-server"/></span>
<span> <span>
<NuxtLink :to="`/backend/${data.via}`" v-text="data.via" /> <NuxtLink :to="`/backend/${data.via}`" v-text="data.via"/>
</span> </span>
</span> </span>
</div> </div>
@@ -130,17 +130,17 @@
<div class="columns is-multiline is-mobile"> <div class="columns is-multiline is-mobile">
<div class="column is-6"> <div class="column is-6">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-passport" /></span> <span class="icon"><i class="fas fa-passport"/></span>
<span> <span>
<span class="is-hidden-mobile">ID:&nbsp;</span> <span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="`/history/${data.id}`" v-text="data.id" /> <NuxtLink :to="`/history/${data.id}`" v-text="data.id"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(data.progress)"> <span class="icon-text" v-if="parseInt(data.progress)">
<span class="icon"><i class="fas fa-bars-progress" /></span> <span class="icon"><i class="fas fa-bars-progress"/></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(data.progress) }}</span> <span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(data.progress) }}</span>
</span> </span>
<span v-else>-</span> <span v-else>-</span>
@@ -149,8 +149,8 @@
<div class="column is-6 has-text-left"> <div class="column is-6 has-text-left">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fas fa-eye-slash" v-if="!data.watched" /> <i class="fas fa-eye-slash" v-if="!data.watched"/>
<i class="fas fa-eye" v-else /> <i class="fas fa-eye" v-else/>
</span> </span>
<span> <span>
<span class="is-hidden-mobile">Status:</span> <span class="is-hidden-mobile">Status:</span>
@@ -160,7 +160,7 @@
</div> </div>
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-envelope" /></span> <span class="icon"><i class="fas fa-envelope"/></span>
<span> <span>
<span class="is-hidden-mobile">Event:</span> <span class="is-hidden-mobile">Event:</span>
{{ ag(data.extra, `${data.via}.event`, 'Unknown') }} {{ ag(data.extra, `${data.via}.event`, 'Unknown') }}
@@ -169,11 +169,11 @@
</div> </div>
<div class="column is-6 has-text-left"> <div class="column is-6 has-text-left">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-calendar" /></span> <span class="icon"><i class="fas fa-calendar"/></span>
<span> <span>
<span class="is-hidden-mobile">Updated:&nbsp;</span> <span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip" <span class="has-tooltip"
v-tooltip="`Backend updated this record at: ${moment.unix(data.updated).format(TOOLTIP_DATE_FORMAT)}`"> v-tooltip="`Backend updated this record at: ${moment.unix(data.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated).fromNow() }} {{ moment.unix(data.updated).fromNow() }}
</span> </span>
</span> </span>
@@ -182,36 +182,36 @@
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text"> <span class="icon-text">
<span class="icon" v-if="'episode' === data.type"><i class="fas fa-tv" /></span> <span class="icon" v-if="'episode' === data.type"><i class="fas fa-tv"/></span>
<span class="icon" v-else><i class="fas fa-film" /></span> <span class="icon" v-else><i class="fas fa-film"/></span>
<span> <span>
<span class="is-hidden-mobile">Type:&nbsp;</span> <span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type', data.type)" v-text="ucFirst(data.type)" /> <NuxtLink :to="makeSearchLink('type', data.type)" v-text="ucFirst(data.type)"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6 has-text-left" v-if="'episode' === data.type"> <div class="column is-6 has-text-left" v-if="'episode' === data.type">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-tv" /></span> <span class="icon"><i class="fas fa-tv"/></span>
<span><span class="is-hidden-mobile">Season:&nbsp;</span> <span><span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season', data.season)" v-text="data.season" /> <NuxtLink :to="makeSearchLink('season', data.season)" v-text="data.season"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6 has-text-right" v-if="'episode' === data.type"> <div class="column is-6 has-text-right" v-if="'episode' === data.type">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-tv" /></span> <span class="icon"><i class="fas fa-tv"/></span>
<span><span class="is-hidden-mobile">Episode:&nbsp;</span> <span><span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode', data.episode)" v-text="data.episode" /> <NuxtLink :to="makeSearchLink('episode', data.episode)" v-text="data.episode"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-12" v-if="data.guids && Object.keys(data.guids).length > 0"> <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-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link" /></span> <span class="icon"><i class="fas fa-link"/></span>
<span>GUIDs:&nbsp;</span> <span>GUIDs:&nbsp;</span>
</span> </span>
<span class="tag mr-1" v-for="(guid, source) in data.guids"> <span class="tag mr-1" v-for="(guid, source) in data.guids">
@@ -223,7 +223,7 @@
<div class="column is-12" v-if="data.rguids && Object.keys(data.rguids).length > 0"> <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-text is-clickable" v-tooltip="'Relative Globally unique identifier for this episode'">
<span class="icon"><i class="fas fa-link" /></span> <span class="icon"><i class="fas fa-link"/></span>
<span>rGUIDs:&nbsp;</span> <span>rGUIDs:&nbsp;</span>
</span> </span>
<span class="tag mr-1" v-for="(guid, source) in data.rguids"> <span class="tag mr-1" v-for="(guid, source) in data.rguids">
@@ -235,7 +235,7 @@
<div class="column is-12" v-if="data.parent && Object.keys(data.parent).length > 0"> <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-text is-clickable" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link" /></span> <span class="icon"><i class="fas fa-link"/></span>
<span>Series GUIDs:&nbsp;</span> <span>Series GUIDs:&nbsp;</span>
</span> </span>
<span class="tag mr-1" v-for="(guid, source) in data.parent"> <span class="tag mr-1" v-for="(guid, source) in data.parent">
@@ -247,27 +247,27 @@
<div class="column is-12" v-if="data?.content_title"> <div class="column is-12" v-if="data?.content_title">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading" /></span> <span class="icon"><i class="fas fa-heading"/></span>
<span class="is-hidden-mobile">Subtitle:&nbsp;</span> <span class="is-hidden-mobile">Subtitle:&nbsp;</span>
<NuxtLink :to="makeSearchLink('subtitle', data.content_title)" v-text="data.content_title" /> <NuxtLink :to="makeSearchLink('subtitle', data.content_title)" v-text="data.content_title"/>
</div> </div>
</div> </div>
<div class="column is-12" v-if="data?.content_path"> <div class="column is-12" v-if="data?.content_path">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-file" /></span> <span class="icon"><i class="fas fa-file"/></span>
<span class="is-hidden-mobile">File:&nbsp;</span> <span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', data.content_path)" v-text="data.content_path" /> <NuxtLink :to="makeSearchLink('path', data.content_path)" v-text="data.content_path"/>
</div> </div>
</div> </div>
<div class="column is-6 has-text-left" v-if="data.created_at"> <div class="column is-6 has-text-left" v-if="data.created_at">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-database" /></span> <span class="icon"><i class="fas fa-database"/></span>
<span> <span>
<span class="is-hidden-mobile">Created:&nbsp;</span> <span class="is-hidden-mobile">Created:&nbsp;</span>
<span class="has-tooltip" <span class="has-tooltip"
v-tooltip="`DB record created at: ${moment.unix(data.created_at).format(TOOLTIP_DATE_FORMAT)}`"> v-tooltip="`DB record created at: ${moment.unix(data.created_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.created_at).fromNow() }} {{ moment.unix(data.created_at).fromNow() }}
</span> </span>
</span> </span>
@@ -276,11 +276,11 @@
<div class="column is-6 has-text-right" v-if="data.updated_at"> <div class="column is-6 has-text-right" v-if="data.updated_at">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-database" /></span> <span class="icon"><i class="fas fa-database"/></span>
<span> <span>
<span class="is-hidden-mobile">Updated:&nbsp;</span> <span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip" <span class="has-tooltip"
v-tooltip="`DB record updated at: ${moment.unix(data.updated_at).format(TOOLTIP_DATE_FORMAT)}`"> v-tooltip="`DB record updated at: ${moment.unix(data.updated_at).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(data.updated_at).fromNow() }} {{ moment.unix(data.updated_at).fromNow() }}
</span> </span>
</span> </span>
@@ -289,20 +289,20 @@
<div class="is-hidden-tablet column is-12" v-if="data?.content_genres && data?.content_genres.length > 0"> <div class="is-hidden-tablet column is-12" v-if="data?.content_genres && data?.content_genres.length > 0">
<div class="is-clickable" :class="{ 'is-text-overflow': !expandGenres }" <div class="is-clickable" :class="{ 'is-text-overflow': !expandGenres }"
@click="expandGenres = !expandGenres"> @click="expandGenres = !expandGenres">
<span class="icon"><i class="fas fa-tag" /></span> <span class="icon"><i class="fas fa-tag"/></span>
<span class="is-hidden-mobile">Genres:&nbsp;</span> <span class="is-hidden-mobile">Genres:&nbsp;</span>
<span class="tag is-info mr-1 is-capitalized" v-for="genre in data.content_genres" <span class="tag is-info mr-1 is-capitalized" v-for="genre in data.content_genres"
:key="`latest-${genre}`" v-text="genre" /> :key="`latest-${genre}`" v-text="genre"/>
</div> </div>
</div> </div>
<div class="is-hidden-tablet column is-12" v-if="data?.content_overview"> <div class="is-hidden-tablet column is-12" v-if="data?.content_overview">
<span class="icon"><i class="fas fa-comment" /></span> <span class="icon"><i class="fas fa-comment"/></span>
<span>Content Summary</span> <span>Content Summary</span>
<br> <br>
<div class="is-clickable" :class="{ 'is-text-overflow': !expandOverview }" <div class="is-clickable" :class="{ 'is-text-overflow': !expandOverview }"
@click="expandOverview = !expandOverview"> @click="expandOverview = !expandOverview">
{{ data.content_overview }} {{ data.content_overview }}
</div> </div>
</div> </div>
@@ -314,40 +314,60 @@
<div class="column is-12" v-if="data?.via && Object.keys(data.metadata).length > 0"> <div class="column is-12" v-if="data?.via && Object.keys(data.metadata).length > 0">
<div class="card" v-for="(item, key) in data.metadata" :key="key" <div class="card" v-for="(item, key) in data.metadata" :key="key"
:class="{ 'is-success': parseInt(item.watched), 'transparent-bg': styleInfo }"> :class="{ 'is-success': parseInt(item.watched), 'transparent-bg': styleInfo }">
<header class="card-header"> <header class="card-header">
<div class="card-header-title is-clickable is-unselectable" @click="item._toggle = !item._toggle"> <div class="card-header-title is-clickable is-unselectable" @click="item._toggle = !item._toggle">
<span class="icon"> <span class="icon">
<i class="fas" :class="{ 'fa-arrow-up': item?._toggle, 'fa-arrow-down': !item?._toggle }" /> <i class="fas" :class="{ 'fa-arrow-up': item?._toggle, 'fa-arrow-down': !item?._toggle }"/>
</span> </span>
&nbsp;
<i class="fas" :class="{
'fa-spinner fa-spin': undefined === item?.validated,
'fa-check has-text-success': true === item?.validated,
'fa-xmark has-text-danger': false === item?.validated,
}"/>&nbsp;
Metadata via Metadata via
</div> </div>
<div class="card-header-icon"> <div class="card-header-icon">
<span class="icon-text"> <div class="field is-grouped">
<span class="icon"><i class="fas fa-server" /></span> <div class="control" v-if="false === item?.validated">
<span> <NuxtLink @click="deleteMetadata(key)">
<NuxtLink :to="`/backend/${key}`" v-text="key" /> <span class="icon-text has-text-danger">
</span> <span class="icon"><i class="fas fa-trash"/></span>
</span> <span>Delete</span>
</span>
</NuxtLink>
</div>
<div class="control">
<span class="icon-text">
<span class="icon"><i class="fas fa-server"/></span>
<span>
<NuxtLink :to="`/backend/${key}`" v-text="key"/>
</span>
</span>
</div>
</div>
</div> </div>
</header> </header>
<div class="card-content" v-if="item?._toggle"> <div class="card-content" v-if="item?._toggle">
<div class="columns is-multiline is-mobile"> <div class="columns is-multiline is-mobile">
<div class="column is-12" v-if="false === item?.validated && item.validated_message">
<span class="has-text-danger">({{ item.validated_message }})</span>
</div>
<div class="column is-6"> <div class="column is-6">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-passport" /></span> <span class="icon"><i class="fas fa-passport"/></span>
<span> <span>
<span class="is-hidden-mobile">ID:&nbsp;</span> <span class="is-hidden-mobile">ID:&nbsp;</span>
<NuxtLink :to="item?.webUrl" target="_blank" v-text="item.id" v-if="item?.webUrl" /> <NuxtLink :to="item?.webUrl" target="_blank" v-text="item.id" v-if="item?.webUrl"/>
<span v-else v-text="item.id" /> <span v-else v-text="item.id"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text" v-if="parseInt(item?.progress)"> <span class="icon-text" v-if="parseInt(item?.progress)">
<span class="icon"><i class="fas fa-bars-progress" /></span> <span class="icon"><i class="fas fa-bars-progress"/></span>
<span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(item.progress) }}</span> <span><span class="is-hidden-mobile">Progress:</span> {{ formatDuration(item.progress) }}</span>
</span> </span>
<span v-else>-</span> <span v-else>-</span>
@@ -356,7 +376,7 @@
<div class="column is-6"> <div class="column is-6">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fas fa-eye-slash" :class="parseInt(item.watched) ? 'fa-eye-slash' : 'fa-eye'" /> <i class="fas fa-eye-slash" :class="parseInt(item.watched) ? 'fa-eye-slash' : 'fa-eye'"/>
</span> </span>
<span> <span>
<span class="is-hidden-mobile">Status:</span> <span class="is-hidden-mobile">Status:</span>
@@ -367,7 +387,7 @@
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-envelope" /></span> <span class="icon"><i class="fas fa-envelope"/></span>
<span> <span>
<span class="is-hidden-mobile">Event:</span> <span class="is-hidden-mobile">Event:</span>
{{ ag(data.extra, `${key}.event`, 'Unknown') }} {{ ag(data.extra, `${key}.event`, 'Unknown') }}
@@ -377,11 +397,11 @@
<div class="column is-6"> <div class="column is-6">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-calendar" /></span> <span class="icon"><i class="fas fa-calendar"/></span>
<span> <span>
<span class="is-hidden-mobile">Updated:&nbsp;</span> <span class="is-hidden-mobile">Updated:&nbsp;</span>
<span class="has-tooltip" <span class="has-tooltip"
v-tooltip="`Backend last activity: ${getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format(TOOLTIP_DATE_FORMAT)}`"> v-tooltip="`Backend last activity: ${getMoment(ag(data.extra, `${key}.received_at`, data.updated)).format(TOOLTIP_DATE_FORMAT)}`">
{{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }} {{ getMoment(ag(data.extra, `${key}.received_at`, data.updated)).fromNow() }}
</span> </span>
</span> </span>
@@ -390,38 +410,38 @@
<div class="column is-6 has-text-right"> <div class="column is-6 has-text-right">
<span class="icon-text"> <span class="icon-text">
<span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv" /></span> <span class="icon" v-if="'episode' === item.type"><i class="fas fa-tv"/></span>
<span class="icon" v-else><i class="fas fa-film" /></span> <span class="icon" v-else><i class="fas fa-film"/></span>
<span> <span>
<span class="is-hidden-mobile">Type:&nbsp;</span> <span class="is-hidden-mobile">Type:&nbsp;</span>
<NuxtLink :to="makeSearchLink('type', item.type)" v-text="ucFirst(item.type)" /> <NuxtLink :to="makeSearchLink('type', item.type)" v-text="ucFirst(item.type)"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6" v-if="'episode' === item.type"> <div class="column is-6" v-if="'episode' === item.type">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-tv" /></span> <span class="icon"><i class="fas fa-tv"/></span>
<span> <span>
<span class="is-hidden-mobile">Season:&nbsp;</span> <span class="is-hidden-mobile">Season:&nbsp;</span>
<NuxtLink :to="makeSearchLink('season', item.season)" v-text="item.season" /> <NuxtLink :to="makeSearchLink('season', item.season)" v-text="item.season"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-6 has-text-right" v-if="'episode' === item.type"> <div class="column is-6 has-text-right" v-if="'episode' === item.type">
<span class="icon-text"> <span class="icon-text">
<span class="icon"><i class="fas fa-tv" /></span> <span class="icon"><i class="fas fa-tv"/></span>
<span> <span>
<span class="is-hidden-mobile">Episode:&nbsp;</span> <span class="is-hidden-mobile">Episode:&nbsp;</span>
<NuxtLink :to="makeSearchLink('episode', item.episode)" v-text="item.episode" /> <NuxtLink :to="makeSearchLink('episode', item.episode)" v-text="item.episode"/>
</span> </span>
</span> </span>
</div> </div>
<div class="column is-12" v-if="item.guids && Object.keys(item.guids).length > 0"> <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-text is-clickable" v-tooltip="'Globally unique identifier for this item'">
<span class="icon"><i class="fas fa-link" /></span> <span class="icon"><i class="fas fa-link"/></span>
<span>GUIDs:&nbsp;</span> <span>GUIDs:&nbsp;</span>
</span> </span>
<span class="tag mr-1" v-for="(guid, source) in item.guids"> <span class="tag mr-1" v-for="(guid, source) in item.guids">
@@ -433,7 +453,7 @@
<div class="column is-12" v-if="item.parent && Object.keys(item.parent).length > 0"> <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="is-clickable icon-text" v-tooltip="'Globally unique identifier for the series'">
<span class="icon"><i class="fas fa-link" /></span> <span class="icon"><i class="fas fa-link"/></span>
<span>Series GUIDs:&nbsp;</span> <span>Series GUIDs:&nbsp;</span>
</span> </span>
<span class="tag mr-1" v-for="(guid, source) in item.parent"> <span class="tag mr-1" v-for="(guid, source) in item.parent">
@@ -445,37 +465,37 @@
<div class="column is-12" v-if="item?.extra?.title"> <div class="column is-12" v-if="item?.extra?.title">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-heading" /></span> <span class="icon"><i class="fas fa-heading"/></span>
<span class="is-hidden-mobile">Subtitle:&nbsp</span> <span class="is-hidden-mobile">Subtitle:&nbsp</span>
<NuxtLink :to="makeSearchLink('subtitle', item.extra.title)" v-text="item.extra.title" /> <NuxtLink :to="makeSearchLink('subtitle', item.extra.title)" v-text="item.extra.title"/>
</div> </div>
</div> </div>
<div class="column is-12" v-if="item?.extra?.genres && item.extra.genres.length > 0"> <div class="column is-12" v-if="item?.extra?.genres && item.extra.genres.length > 0">
<div class="is-clickable" :class="{ 'is-text-overflow': !item?.expandGenres }" <div class="is-clickable" :class="{ 'is-text-overflow': !item?.expandGenres }"
@click="item.expandGenres = !item?.expandGenres"> @click="item.expandGenres = !item?.expandGenres">
<span class="icon"><i class="fas fa-tag" /></span> <span class="icon"><i class="fas fa-tag"/></span>
<span class="is-hidden-mobile">Genres:&nbsp;</span> <span class="is-hidden-mobile">Genres:&nbsp;</span>
<span class="tag is-info mr-1 is-capitalized" v-for="genre in item.extra.genres" <span class="tag is-info mr-1 is-capitalized" v-for="genre in item.extra.genres"
:key="`${item.id}-${genre}`" v-text="genre" /> :key="`${item.id}-${genre}`" v-text="genre"/>
</div> </div>
</div> </div>
<div class="column is-12" v-if="item?.extra?.overview"> <div class="column is-12" v-if="item?.extra?.overview">
<span class="icon"><i class="fas fa-comment" /></span> <span class="icon"><i class="fas fa-comment"/></span>
<span>Content Summary</span> <span>Content Summary</span>
<br> <br>
<div class="is-clickable" :class="{ 'is-text-overflow': !item?.expandOverview }" <div class="is-clickable" :class="{ 'is-text-overflow': !item?.expandOverview }"
@click="item.expandOverview = !item?.expandOverview"> @click="item.expandOverview = !item?.expandOverview">
{{ item.extra.overview }} {{ item.extra.overview }}
</div> </div>
</div> </div>
<div class="column is-12" v-if="item?.path"> <div class="column is-12" v-if="item?.path">
<div class="is-text-overflow"> <div class="is-text-overflow">
<span class="icon"><i class="fas fa-file" /></span> <span class="icon"><i class="fas fa-file"/></span>
<span class="is-hidden-mobile">File:&nbsp;</span> <span class="is-hidden-mobile">File:&nbsp;</span>
<NuxtLink :to="makeSearchLink('path', item.path)" v-text="item.path" /> <NuxtLink :to="makeSearchLink('path', item.path)" v-text="item.path"/>
</div> </div>
</div> </div>
@@ -490,8 +510,8 @@
<span class="title is-4 is-clickable" @click="showRawData = !showRawData"> <span class="title is-4 is-clickable" @click="showRawData = !showRawData">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i v-if="showRawData" class="fas fa-arrow-up" /> <i v-if="showRawData" class="fas fa-arrow-up"/>
<i v-else class="fas fa-arrow-down" /> <i v-else class="fas fa-arrow-down"/>
</span> </span>
<span>Show raw unfiltered data</span> <span>Show raw unfiltered data</span>
</span> </span>
@@ -499,23 +519,23 @@
<p class="subtitle">Useful for debugging.</p> <p class="subtitle">Useful for debugging.</p>
<div v-if="showRawData" class="mt-2" style="position: relative; max-height: 400px; overflow-y: auto;"> <div v-if="showRawData" class="mt-2" style="position: relative; max-height: 400px; overflow-y: auto;">
<code class="is-terminal is-block is-pre-wrap p-4">{{ <code class="is-terminal is-block is-pre-wrap p-4">{{
JSON.stringify(Object.keys(data) JSON.stringify(Object.keys(data)
.filter(key => !['files', 'hardware', 'content_exists', '_toggle'].includes(key)) .filter(key => !['files', 'hardware', 'content_exists', '_toggle'].includes(key))
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = data[key]; obj[key] = data[key];
return obj; return obj;
}, {}), null, 2) }, {}), null, 2)
}}</code> }}</code>
<button class="button m-4" v-tooltip="'Copy text'" @click="() => copyText(JSON.stringify(data, null, 2))" <button class="button m-4" v-tooltip="'Copy text'" @click="() => copyText(JSON.stringify(data, null, 2))"
style="position: absolute; top:0; right:0;"> style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy" /></span> <span class="icon"><i class="fas fa-copy"/></span>
</button> </button>
</div> </div>
</div> </div>
<div class="column is-12"> <div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips" <Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle"> @toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul> <ul>
<li> <li>
To see if your media backends are reporting different metadata for the same file, click on the file link To see if your media backends are reporting different metadata for the same file, click on the file link
@@ -527,7 +547,7 @@
</li> </li>
<li> <li>
<code>rGUIDSs</code> are relative globally unique identifiers for episodes based on <code>series <code>rGUIDSs</code> are relative globally unique identifiers for episodes based on <code>series
GUID</code>. They are formatted as <code>GUID://seriesID/season_number/episode_number</code>. We use GUID</code>. They are formatted as <code>GUID://seriesID/season_number/episode_number</code>. We use
<code>rGUIDs</code>, to identify specific episode. This is more reliable than using episode specific <code>rGUIDs</code>, to identify specific episode. This is more reliable than using episode specific
<code>GUID</code>, as they are often misreported in the source data. <code>GUID</code>, as they are often misreported in the source data.
</li> </li>
@@ -563,12 +583,12 @@ import {
ucFirst ucFirst
} from '~/utils/index' } from '~/utils/index'
import moment from 'moment' import moment from 'moment'
import { useBreakpoints, useStorage } from '@vueuse/core' import {useBreakpoints, useStorage} from '@vueuse/core'
import Message from '~/components/Message' import Message from '~/components/Message'
const id = useRoute().params.id const id = useRoute().params.id
useHead({ title: `History : ${id}` }) useHead({title: `History : ${id}`})
const isLoading = ref(true) const isLoading = ref(true)
const showRawData = ref(false) const showRawData = ref(false)
@@ -576,8 +596,8 @@ const show_page_tips = useStorage('show_page_tips', true)
const api_show_photos = useStorage('api_show_photos', true) const api_show_photos = useStorage('api_show_photos', true)
const show_history_page_warning = useStorage('show_history_page_warning', true) const show_history_page_warning = useStorage('show_history_page_warning', true)
const isDeleting = ref(false) const isDeleting = ref(false)
const breakpoints = useBreakpoints({ mobile: 0, desktop: 640 }) const breakpoints = useBreakpoints({mobile: 0, desktop: 640})
const loadedImages = ref({ poster: null, background: null }) const loadedImages = ref({poster: null, background: null})
const expandOverview = ref(false) const expandOverview = ref(false)
const expandGenres = ref(false) const expandGenres = ref(false)
@@ -619,7 +639,7 @@ const loadContent = async (id) => {
if (200 !== response.status) { if (200 !== response.status) {
notification('Error', 'Error loading data', `${json.error.code}: ${json.error.message}`); notification('Error', 'Error loading data', `${json.error.code}: ${json.error.message}`);
if (404 === response.status) { if (404 === response.status) {
await navigateTo({ name: 'history' }) await navigateTo({name: 'history'})
} }
return return
} }
@@ -627,8 +647,10 @@ const loadContent = async (id) => {
data.value = json data.value = json
data.value._toggle = true data.value._toggle = true
useHead({ title: `History : ${makeName(json) ?? id}` }) useHead({title: `History : ${makeName(json) ?? id}`})
await loadImage() await loadImage()
await nextTick();
await validateItem()
} }
watch(breakpoints.active(), async () => await loadImage()) watch(breakpoints.active(), async () => await loadImage())
@@ -676,7 +698,7 @@ const deleteItem = async (item) => {
isDeleting.value = true isDeleting.value = true
try { try {
const response = await request(`/history/${id}`, { method: 'DELETE' }) const response = await request(`/history/${id}`, {method: 'DELETE'})
if (200 !== response.status) { if (200 !== response.status) {
const json = await response.json() const json = await response.json()
@@ -685,7 +707,7 @@ const deleteItem = async (item) => {
} }
notification('success', 'Success!', `Deleted '${makeName(item)}'.`) notification('success', 'Success!', `Deleted '${makeName(item)}'.`)
await navigateTo({ name: 'history' }) await navigateTo({name: 'history'})
} catch (e) { } catch (e) {
notification('error', 'Error', e.message) notification('error', 'Error', e.message)
} finally { } finally {
@@ -720,6 +742,48 @@ const toggleWatched = async () => {
} }
} }
const validateItem = async () => {
try {
const response = await request(`/history/${id}/validate`)
if (!response.ok) {
return
}
const json = await response.json()
for (const [backend, item] of Object.entries(json)) {
if (data.value.metadata[backend] === undefined) {
continue
}
data.value.metadata[backend]['validated'] = item.status
data.value.metadata[backend]['validated_message'] = item.message
}
} catch (e) {
}
}
const deleteMetadata = async backend => {
if (!confirm(`Remove metadata from '${backend}'?`)) {
return
}
try {
const response = await request(`/history/${id}/metadata/${backend}`, {method: 'DELETE'})
if (200 !== response.status) {
const json = await parse_api_response(response)
notification('error', 'Error', `${json.error.code}: ${json.error.message}`)
return
}
notification('success', 'Success!', `Deleted '${backend}' metadata.`)
await loadContent(id);
} catch (e) {
notification('error', 'Error', `Request error. ${e}`)
}
}
const getMoment = (time) => time.toString().length < 13 ? moment.unix(time) : moment(time) const getMoment = (time) => time.toString().length < 13 ? moment.unix(time) : moment(time)
const headerTitle = computed(() => isLoading.value ? id : makeName(data.value)) const headerTitle = computed(() => isLoading.value ? id : makeName(data.value))

View File

@@ -12,6 +12,7 @@ use App\Libs\Attributes\Route\Get;
use App\Libs\Attributes\Route\Route; use App\Libs\Attributes\Route\Route;
use App\Libs\Container; use App\Libs\Container;
use App\Libs\DataUtil; use App\Libs\DataUtil;
use App\Libs\Entity\StateEntity;
use App\Libs\Entity\StateInterface as iState; use App\Libs\Entity\StateInterface as iState;
use App\Libs\Enums\Http\Method; use App\Libs\Enums\Http\Method;
use App\Libs\Enums\Http\Status; use App\Libs\Enums\Http\Status;
@@ -19,12 +20,15 @@ use App\Libs\Exceptions\RuntimeException;
use App\Libs\Guid; use App\Libs\Guid;
use App\Libs\Mappers\Import\DirectMapper; use App\Libs\Mappers\Import\DirectMapper;
use App\Libs\Mappers\ImportInterface as iImport; use App\Libs\Mappers\ImportInterface as iImport;
use App\Libs\Options;
use App\Libs\Traits\APITraits; use App\Libs\Traits\APITraits;
use DateInterval;
use JsonException; use JsonException;
use PDO; use PDO;
use Psr\Http\Message\ResponseInterface as iResponse; use Psr\Http\Message\ResponseInterface as iResponse;
use Psr\Http\Message\ServerRequestInterface as iRequest; use Psr\Http\Message\ServerRequestInterface as iRequest;
use Psr\Log\LoggerInterface as iLogger; use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use SplFileInfo; use SplFileInfo;
use Throwable; use Throwable;
@@ -689,6 +693,105 @@ final class Index
return $this->read($request, $id); return $this->read($request, $id);
} }
#[Get(self::URL . '/{id:\d+}/validate[/]', name: 'history.validate')]
public function validate_item(iCache $cache, iRequest $request, string $id): iResponse
{
try {
$userContext = $this->getUserContext(request: $request, mapper: $this->mapper, logger: $this->logger);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$entity = Container::get(iState::class)::fromArray([iState::COLUMN_ID => $id]);
if (null === ($item = $userContext->db->get($entity))) {
return api_error('Item Not found', Status::NOT_FOUND);
}
$cacheKey = r('validate_item_{id}_{user}_{backends}', [
'id' => $item->id,
'user' => $userContext->name,
'backends' => md5(implode(',', array_keys($item->getMetadata()))),
]);
try {
if (null !== ($cached = $cache->get($cacheKey))) {
return api_response(Status::OK, $cached, headers: ['X-Cache' => 'HIT']);
}
} catch (Throwable) {
// Ignore cache errors.
}
$validation = [];
foreach ($item->getMetadata() as $name => $metadata) {
$id = ag($metadata, StateEntity::COLUMN_ID, null);
$validation[$name] = [
'id' => $id,
'status' => false,
'message' => 'Item not found.',
];
if (null === $userContext->config->get($name)) {
$validation[$name]['message'] = 'Backend not found.';
continue;
}
if (null === $id) {
$validation[$name]['message'] = 'Item ID is missing.';
continue;
}
try {
$client = $this->getClient(name: $name, userContext: $userContext);
$item = $client->getMetadata($id, [Options::NO_LOGGING => true]);
if (count($item) > 0) {
$validation[$name]['status'] = true;
$validation[$name]['message'] = 'Item found.';
} else {
$validation[$name]['status'] = false;
$validation[$name]['message'] = 'Item not found.';
}
} catch (Throwable $e) {
$validation[$name]['message'] = $e->getMessage();
}
}
try {
$cache->set($cacheKey, $validation, new DateInterval('PT10M'));
} catch (Throwable) {
// Ignore cache errors.
}
return api_response(Status::OK, $validation, headers: ['X-Cache' => 'MISS']);
}
#[Delete(self::URL . '/{id:\d+}/metadata/{backend}[/]', name: 'history.metadata.delete')]
public function delete_item_metadata(iCache $cache, iRequest $request, string $id, string $backend): iResponse
{
try {
$userContext = $this->getUserContext(request: $request, mapper: $this->mapper, logger: $this->logger);
} catch (RuntimeException $e) {
return api_error($e->getMessage(), Status::NOT_FOUND);
}
$entity = Container::get(iState::class)::fromArray([iState::COLUMN_ID => $id]);
if (null === ($item = $userContext->db->get($entity))) {
return api_error('Item Not found', Status::NOT_FOUND);
}
if (null === ($item->getMetadata($backend) ?? null)) {
return api_error('Item metadata not found.', Status::NOT_FOUND);
}
$item->metadata = ag_delete($item->getMetadata(), $backend);
$userContext->db->update($item);
return $this->read($request, $id);
}
#[Get(self::URL . '/{id:\d+}/images/{type:poster|background}[/]', name: 'history.item.images')] #[Get(self::URL . '/{id:\d+}/images/{type:poster|background}[/]', name: 'history.item.images')]
public function images(iRequest $request, string $id, string $type): iResponse public function images(iRequest $request, string $id, string $type): iResponse
{ {