From c3d427c1346f7514b7d5d2b589953bebfc12f194 Mon Sep 17 00:00:00 2001 From: Fleeting Orchard Date: Mon, 9 Aug 2021 18:04:50 +1000 Subject: [PATCH] Add "Scenes duration" stat on statistics page --- graphql/documents/queries/misc.graphql | 1 + graphql/schema/types/stats.graphql | 1 + pkg/api/resolver.go | 2 + pkg/models/mocks/SceneReaderWriter.go | 21 +++++ pkg/models/scene.go | 1 + pkg/sqlite/scene.go | 4 + ui/v2.5/src/components/Stats.tsx | 39 +++++---- ui/v2.5/src/locales/en-GB.json | 1 + ui/v2.5/src/utils/text.ts | 107 +++++++++++++++++++++++++ 9 files changed, 163 insertions(+), 14 deletions(-) diff --git a/graphql/documents/queries/misc.graphql b/graphql/documents/queries/misc.graphql index 0816d39f5a0..b8b4871d14a 100644 --- a/graphql/documents/queries/misc.graphql +++ b/graphql/documents/queries/misc.graphql @@ -41,6 +41,7 @@ query Stats { stats { scene_count, scenes_size, + scenes_duration, image_count, images_size, gallery_count, diff --git a/graphql/schema/types/stats.graphql b/graphql/schema/types/stats.graphql index d127871f2cb..fcadd54a78a 100644 --- a/graphql/schema/types/stats.graphql +++ b/graphql/schema/types/stats.graphql @@ -1,6 +1,7 @@ type StatsResultType { scene_count: Int! scenes_size: Float! + scenes_duration: Float! image_count: Int! images_size: Float! gallery_count: Int! diff --git a/pkg/api/resolver.go b/pkg/api/resolver.go index 07534fc1ef0..ed15a1d2148 100644 --- a/pkg/api/resolver.go +++ b/pkg/api/resolver.go @@ -138,6 +138,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err tagsQB := repo.Tag() scenesCount, _ := scenesQB.Count() scenesSize, _ := scenesQB.Size() + scenesDuration, _ := scenesQB.Duration() imageCount, _ := imageQB.Count() imageSize, _ := imageQB.Size() galleryCount, _ := galleryQB.Count() @@ -149,6 +150,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err ret = models.StatsResultType{ SceneCount: scenesCount, ScenesSize: scenesSize, + ScenesDuration: scenesDuration, ImageCount: imageCount, ImagesSize: imageSize, GalleryCount: galleryCount, diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index 796c2387844..32699951843 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -254,6 +254,27 @@ func (_m *SceneReaderWriter) DestroyCover(sceneID int) error { return r0 } +// Duration provides a mock function with given fields: +func (_m *SceneReaderWriter) Duration() (float64, error) { + ret := _m.Called() + + var r0 float64 + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Find provides a mock function with given fields: id func (_m *SceneReaderWriter) Find(id int) (*models.Scene, error) { ret := _m.Called(id) diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 8e77b2497a6..60345fce92b 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -15,6 +15,7 @@ type SceneReader interface { CountByMovieID(movieID int) (int, error) Count() (int, error) Size() (float64, error) + Duration() (float64, error) // SizeCount() (string, error) CountByStudioID(studioID int) (int, error) CountByTagID(tagID int) (int, error) diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 2c34936a061..f42e13c5559 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -272,6 +272,10 @@ func (qb *sceneQueryBuilder) Size() (float64, error) { return qb.runSumQuery("SELECT SUM(cast(size as double)) as sum FROM scenes", nil) } +func (qb *sceneQueryBuilder) Duration() (float64, error) { + return qb.runSumQuery("SELECT SUM(cast(duration as double)) as sum FROM scenes", nil) +} + func (qb *sceneQueryBuilder) CountByStudioID(studioID int) (int, error) { args := []interface{}{studioID} return qb.runCountQuery(qb.buildCountQuery(scenesForStudioQuery), args) diff --git a/ui/v2.5/src/components/Stats.tsx b/ui/v2.5/src/components/Stats.tsx index dffcc1b9d62..f672b8b527d 100644 --- a/ui/v2.5/src/components/Stats.tsx +++ b/ui/v2.5/src/components/Stats.tsx @@ -41,34 +41,45 @@ export const Stats: React.FC = () => {

- - {` ${TextUtils.formatFileSizeUnit(imagesSize.unit)}`} +

- +

- + {` ${TextUtils.secondsAsTimeString(data.stats.scenes_duration, 3)}`}

- + +

+
+
+

+ +

+

+

- + + {` ${TextUtils.formatFileSizeUnit(imagesSize.unit)}`}

- +

@@ -81,10 +92,10 @@ export const Stats: React.FC = () => {

- +

- +

diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index b29a24db705..35223a6e78a 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -555,6 +555,7 @@ "scene_count": "Scene Count", "scene_id": "Scene ID", "scenes": "Scenes", + "scenes-duration": "Scenes duration", "scenes-size": "Scenes size", "scenes_updated_at": "Scene Updated At", "sceneTagger": "Scene Tagger", diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index aecac9f8ee6..e90de3cd118 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -35,6 +35,112 @@ const fileSize = (bytes: number = 0) => { }; }; +class DurationUnit { + static readonly SECOND: DurationUnit = new DurationUnit( + "second", + "seconds", + "s", + 1 + ); + static readonly MINUTE: DurationUnit = new DurationUnit( + "minute", + "minutes", + "m", + 60 + ); + static readonly HOUR: DurationUnit = new DurationUnit( + "hour", + "hours", + "h", + DurationUnit.MINUTE.secs * 60 + ); + static readonly DAY: DurationUnit = new DurationUnit( + "day", + "days", + "D", + DurationUnit.HOUR.secs * 24 + ); + static readonly WEEK: DurationUnit = new DurationUnit( + "week", + "weeks", + "W", + DurationUnit.DAY.secs * 7 + ); + static readonly MONTH: DurationUnit = new DurationUnit( + "month", + "months", + "M", + DurationUnit.DAY.secs * 30 + ); + static readonly YEAR: DurationUnit = new DurationUnit( + "year", + "years", + "Y", + DurationUnit.DAY.secs * 365 + ); + + static readonly DURATIONS: DurationUnit[] = [ + DurationUnit.SECOND, + DurationUnit.MINUTE, + DurationUnit.HOUR, + DurationUnit.DAY, + DurationUnit.WEEK, + DurationUnit.MONTH, + DurationUnit.YEAR, + ]; + + private constructor( + private readonly singular: string, + private readonly plural: string, + private readonly shortString: string, + public secs: number + ) {} + + toString() { + return this.shortString; + } +} + +class DurationCount { + public constructor( + public readonly count: number, + public readonly duration: DurationUnit + ) {} + + toString() { + return this.count.toString() + this.duration.toString(); + } +} + +const secondsAsTime = (seconds: number = 0): DurationCount[] => { + if (Number.isNaN(parseFloat(String(seconds))) || !Number.isFinite(seconds)) + return [new DurationCount(0, DurationUnit.DURATIONS[0])]; + + const result = []; + let remainingSeconds = seconds; + // Run down the possible durations and pull them out + for (let i = DurationUnit.DURATIONS.length - 1; i >= 0; i--) { + const q = Math.floor(remainingSeconds / DurationUnit.DURATIONS[i].secs); + if (q !== 0) { + remainingSeconds %= DurationUnit.DURATIONS[i].secs; + result.push(new DurationCount(q, DurationUnit.DURATIONS[i])); + } + } + return result; +}; + +const timeAsString = (time: DurationCount[]): string => { + return time.join(" "); +}; + +const secondsAsTimeString = ( + seconds: number = 0, + maxUnitCount: number = 2 +): string => { + const timeArray = secondsAsTime(seconds).slice(0, maxUnitCount); + return timeAsString(timeArray); +}; + const formatFileSizeUnit = (u: Unit) => { const i = Units.indexOf(u); return shortUnits[i]; @@ -206,6 +312,7 @@ const TextUtils = { instagramURL, formatDate, capitalize, + secondsAsTimeString, }; export default TextUtils;