timeline: show video duration

pull/221/head
Varun Patil 2022-11-09 19:48:03 -08:00
parent c6766833b5
commit 06d226432f
7 changed files with 135 additions and 10 deletions

View File

@ -102,7 +102,7 @@ trait TimelineQueryDays
// We don't actually use m.datetaken here, but postgres
// needs that all fields in ORDER BY are also in SELECT
// when using DISTINCT on selected fields
$query->select($fileid, 'm.isvideo', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
$query->select($fileid, 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
->from('memories', 'm')
;
@ -198,11 +198,13 @@ trait TimelineQueryDays
// Convert field types
$row['fileid'] = (int) $row['fileid'];
$row['isvideo'] = (int) $row['isvideo'];
$row['video_duration'] = (int) $row['video_duration'];
$row['dayid'] = (int) $row['dayid'];
$row['w'] = (int) $row['w'];
$row['h'] = (int) $row['h'];
if (!$row['isvideo']) {
unset($row['isvideo']);
unset($row['video_duration']);
}
if ($row['categoryid']) {
$row['isfavorite'] = 1;

View File

@ -97,6 +97,12 @@ class TimelineWrite
$dateTaken = gmdate('Y-m-d H:i:s', $dateTaken);
[$w, $h] = Exif::getDimensions($exif);
// Video parameters
$videoDuration = 0;
if ($isvideo) {
$videoDuration = round($exif['Duration'] ?? $exif['TrackDuration'] ?? 0);
}
// Store raw metadata in the database
// We need to remove blacklisted fields to prevent leaking info
unset($exif['SourceFile'], $exif['FileName'], $exif['ExifToolVersion'], $exif['Directory'], $exif['FileSize'], $exif['FileModifyDate'], $exif['FileAccessDate'], $exif['FileInodeChangeDate'], $exif['FilePermissions']);
@ -124,6 +130,7 @@ class TimelineWrite
->set('datetaken', $query->createNamedParameter($dateTaken, IQueryBuilder::PARAM_STR))
->set('mtime', $query->createNamedParameter($mtime, IQueryBuilder::PARAM_INT))
->set('isvideo', $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT))
->set('video_duration', $query->createNamedParameter($videoDuration, IQueryBuilder::PARAM_INT))
->set('w', $query->createNamedParameter($w, IQueryBuilder::PARAM_INT))
->set('h', $query->createNamedParameter($h, IQueryBuilder::PARAM_INT))
->set('exif', $query->createNamedParameter($exifJson, IQueryBuilder::PARAM_STR))
@ -141,6 +148,7 @@ class TimelineWrite
'datetaken' => $query->createNamedParameter($dateTaken, IQueryBuilder::PARAM_STR),
'mtime' => $query->createNamedParameter($mtime, IQueryBuilder::PARAM_INT),
'isvideo' => $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT),
'video_duration' => $query->createNamedParameter($videoDuration, IQueryBuilder::PARAM_INT),
'w' => $query->createNamedParameter($w, IQueryBuilder::PARAM_INT),
'h' => $query->createNamedParameter($h, IQueryBuilder::PARAM_INT),
'exif' => $query->createNamedParameter($exifJson, IQueryBuilder::PARAM_STR),

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Your name <your@email.com>
* @author Your name <your@email.com>
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Memories\Migration;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version400700Date20221110030909 extends SimpleMigrationStep
{
/**
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
/**
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('memories');
$table->addColumn('video_duration', Types::INTEGER, [
'notnull' => true,
'default' => 0,
]);
return $schema;
}
/**
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
}

View File

@ -184,7 +184,6 @@ class VideoContentSetup {
updateRotation(content, val?: number) {
const rotation = val ?? Number(content.data.exif?.Rotation);
const shouldRotate = content.videojs?.src().includes("m3u8");
console.log("Video.js: Rotation", rotation, shouldRotate);
if (rotation && shouldRotate) {
let transform = `rotate(${rotation}deg)`;

View File

@ -16,7 +16,13 @@
@click="toggleSelect"
/>
<Video :size="22" v-if="data.flag & c.FLAG_IS_VIDEO" />
<div class="video" v-if="data.flag & c.FLAG_IS_VIDEO">
<span v-if="data.video_duration" class="time">
{{ videoDuration }}
</span>
<Video :size="22" />
</div>
<Star :size="22" v-if="data.flag & c.FLAG_IS_FAVORITE" />
<div
@ -43,14 +49,17 @@
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
import GlobalMixin from "../../mixins/GlobalMixin";
import { getPreviewUrl } from "../../services/FileUtils";
import { IDay, IPhoto } from "../../types";
import * as utils from "../../services/Utils";
import errorsvg from "../../assets/error.svg";
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
import Star from "vue-material-design-icons/Star.vue";
import Video from "vue-material-design-icons/PlayCircleOutline.vue";
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
import errorsvg from "../../assets/error.svg";
import GlobalMixin from "../../mixins/GlobalMixin";
import { getPreviewUrl } from "../../services/FileUtils";
import { IDay, IPhoto } from "../../types";
@Component({
components: {
@ -91,6 +100,13 @@ export default class Photo extends Mixins(GlobalMixin) {
this.refresh();
}
get videoDuration() {
if (this.data.video_duration) {
return utils.getDurationStr(this.data.video_duration);
}
return null;
}
async refresh() {
this.src = await this.getSrc();
}
@ -274,7 +290,7 @@ $icon-size: $icon-half-size * 2;
color: var(--color-primary);
}
}
.play-circle-outline-icon,
.video,
.star-icon {
position: absolute;
z-index: 100;
@ -282,12 +298,23 @@ $icon-size: $icon-half-size * 2;
transition: transform 0.15s ease;
filter: invert(1) brightness(100);
}
.play-circle-outline-icon {
.video {
position: absolute;
top: var(--icon-dist);
right: var(--icon-dist);
.p-outer.selected > & {
transform: translate(-$icon-size, $icon-size);
}
display: flex;
align-items: center;
justify-content: center;
.time {
font-size: 0.75em;
font-weight: bold;
margin-right: 3px;
}
}
.star-icon {
bottom: var(--icon-dist);

View File

@ -69,6 +69,26 @@ export function getFromNowStr(date: Date) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
/** Convert number of seconds to time string */
export function getDurationStr(sec: number) {
let hours = Math.floor(sec / 3600);
let minutes: number | string = Math.floor((sec - hours * 3600) / 60);
let seconds: number | string = sec - hours * 3600 - minutes * 60;
if (seconds < 10) {
seconds = "0" + seconds;
}
if (hours > 0) {
if (minutes < 10) {
minutes = "0" + minutes;
}
return `${hours}:${minutes}:${seconds}`;
}
return `${minutes}:${seconds}`;
}
/**
* Returns a hash code from a string
* @param {String} str The string to hash.

View File

@ -77,6 +77,8 @@ export type IPhoto = {
/** Video flag from server */
isvideo?: boolean;
/** Video duration from server */
video_duration?: number;
/** Favorite flag from server */
isfavorite?: boolean;
/** Is this a folder */