From 50cb6a9c7924ab96600f627062b9e245a9257e39 Mon Sep 17 00:00:00 2001 From: FleetingOrchard <55274008+FleetingOrchard@users.noreply.github.com> Date: Thu, 26 Aug 2021 13:37:08 +1000 Subject: [PATCH] Add duration statistics to stats page (#1626) --- 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 + .../src/components/Changelog/versions/v090.md | 1 + ui/v2.5/src/components/Stats.tsx | 51 +++++---- ui/v2.5/src/locales/en-GB.json | 1 + ui/v2.5/src/utils/text.ts | 107 ++++++++++++++++++ 10 files changed, 170 insertions(+), 20 deletions(-) diff --git a/graphql/documents/queries/misc.graphql b/graphql/documents/queries/misc.graphql index 0816d39f5..b8b4871d1 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 d127871f2..fcadd54a7 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 07534fc1e..ed15a1d21 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 796c23878..326999518 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 8e77b2497..60345fce9 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 cd56e9db2..92cdd5c2c 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -274,6 +274,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/Changelog/versions/v090.md b/ui/v2.5/src/components/Changelog/versions/v090.md index dfda34f86..2b69e11af 100644 --- a/ui/v2.5/src/components/Changelog/versions/v090.md +++ b/ui/v2.5/src/components/Changelog/versions/v090.md @@ -12,6 +12,7 @@ * Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568)) ### 🎨 Improvements +* Added total scenes duration to Stats page. ([#1626](https://github.com/stashapp/stash/pull/1626)) * Move Play Selected Scenes, and Add/Remove Gallery Image buttons to button toolbar. ([#1673](https://github.com/stashapp/stash/pull/1673)) * Added image and gallery counts to tag list view. ([#1672](https://github.com/stashapp/stash/pull/1672)) * Prompt when leaving gallery and image edit pages with unsaved changes. ([#1654](https://github.com/stashapp/stash/pull/1654), [#1669](https://github.com/stashapp/stash/pull/1669)) diff --git a/ui/v2.5/src/components/Stats.tsx b/ui/v2.5/src/components/Stats.tsx index dffcc1b9d..f672b8b52 100644 --- a/ui/v2.5/src/components/Stats.tsx +++ b/ui/v2.5/src/components/Stats.tsx @@ -39,6 +39,35 @@ export const Stats: React.FC = () => {

+
+

+ +

+

+ +

+
+
+

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

+

+ +

+
+
+

+ +

+

+ +

+
+ +

{

-
-

- -

-

- -

-
-
-
-
-

- -

-

- -

-

@@ -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 34b58e700..060032cdc 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -586,6 +586,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 aecac9f8e..e90de3cd1 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;