Add duration statistics to stats page (#1626)

This commit is contained in:
FleetingOrchard
2021-08-26 13:37:08 +10:00
committed by GitHub
parent 45a9aabdaf
commit 50cb6a9c79
10 changed files with 170 additions and 20 deletions

View File

@@ -41,6 +41,7 @@ query Stats {
stats { stats {
scene_count, scene_count,
scenes_size, scenes_size,
scenes_duration,
image_count, image_count,
images_size, images_size,
gallery_count, gallery_count,

View File

@@ -1,6 +1,7 @@
type StatsResultType { type StatsResultType {
scene_count: Int! scene_count: Int!
scenes_size: Float! scenes_size: Float!
scenes_duration: Float!
image_count: Int! image_count: Int!
images_size: Float! images_size: Float!
gallery_count: Int! gallery_count: Int!

View File

@@ -138,6 +138,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err
tagsQB := repo.Tag() tagsQB := repo.Tag()
scenesCount, _ := scenesQB.Count() scenesCount, _ := scenesQB.Count()
scenesSize, _ := scenesQB.Size() scenesSize, _ := scenesQB.Size()
scenesDuration, _ := scenesQB.Duration()
imageCount, _ := imageQB.Count() imageCount, _ := imageQB.Count()
imageSize, _ := imageQB.Size() imageSize, _ := imageQB.Size()
galleryCount, _ := galleryQB.Count() galleryCount, _ := galleryQB.Count()
@@ -149,6 +150,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err
ret = models.StatsResultType{ ret = models.StatsResultType{
SceneCount: scenesCount, SceneCount: scenesCount,
ScenesSize: scenesSize, ScenesSize: scenesSize,
ScenesDuration: scenesDuration,
ImageCount: imageCount, ImageCount: imageCount,
ImagesSize: imageSize, ImagesSize: imageSize,
GalleryCount: galleryCount, GalleryCount: galleryCount,

View File

@@ -254,6 +254,27 @@ func (_m *SceneReaderWriter) DestroyCover(sceneID int) error {
return r0 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 // Find provides a mock function with given fields: id
func (_m *SceneReaderWriter) Find(id int) (*models.Scene, error) { func (_m *SceneReaderWriter) Find(id int) (*models.Scene, error) {
ret := _m.Called(id) ret := _m.Called(id)

View File

@@ -15,6 +15,7 @@ type SceneReader interface {
CountByMovieID(movieID int) (int, error) CountByMovieID(movieID int) (int, error)
Count() (int, error) Count() (int, error)
Size() (float64, error) Size() (float64, error)
Duration() (float64, error)
// SizeCount() (string, error) // SizeCount() (string, error)
CountByStudioID(studioID int) (int, error) CountByStudioID(studioID int) (int, error)
CountByTagID(tagID int) (int, error) CountByTagID(tagID int) (int, error)

View File

@@ -274,6 +274,10 @@ func (qb *sceneQueryBuilder) Size() (float64, error) {
return qb.runSumQuery("SELECT SUM(cast(size as double)) as sum FROM scenes", nil) 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) { func (qb *sceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
args := []interface{}{studioID} args := []interface{}{studioID}
return qb.runCountQuery(qb.buildCountQuery(scenesForStudioQuery), args) return qb.runCountQuery(qb.buildCountQuery(scenesForStudioQuery), args)

View File

@@ -12,6 +12,7 @@
* Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568)) * Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568))
### 🎨 Improvements ### 🎨 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)) * 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)) * 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)) * 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))

View File

@@ -39,6 +39,35 @@ export const Stats: React.FC = () => {
<FormattedMessage id="scenes" defaultMessage="Scenes" /> <FormattedMessage id="scenes" defaultMessage="Scenes" />
</p> </p>
</div> </div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.movie_count} />
</p>
<p className="heading">
<FormattedMessage id="movies" />
</p>
</div>
<div className="stats-element">
<p className="title">
{` ${TextUtils.secondsAsTimeString(data.stats.scenes_duration, 3)}`}
</p>
<p className="heading">
<FormattedMessage
id="scenes-duration"
defaultMessage="Scenes duration"
/>
</p>
</div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.performer_count} />
</p>
<p className="heading">
<FormattedMessage id="performers" />
</p>
</div>
</div>
<div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber <FormattedNumber
@@ -53,24 +82,6 @@ export const Stats: React.FC = () => {
<FormattedMessage id="images-size" defaultMessage="Images size" /> <FormattedMessage id="images-size" defaultMessage="Images size" />
</p> </p>
</div> </div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.image_count} />
</p>
<p className="heading">
<FormattedMessage id="images" />
</p>
</div>
</div>
<div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.movie_count} />
</p>
<p className="heading">
<FormattedMessage id="movies" />
</p>
</div>
<div className="stats-element"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.gallery_count} /> <FormattedNumber value={data.stats.gallery_count} />
@@ -81,10 +92,10 @@ export const Stats: React.FC = () => {
</div> </div>
<div className="stats-element"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.performer_count} /> <FormattedNumber value={data.stats.image_count} />
</p> </p>
<p className="heading"> <p className="heading">
<FormattedMessage id="performers" /> <FormattedMessage id="images" />
</p> </p>
</div> </div>
<div className="stats-element"> <div className="stats-element">

View File

@@ -586,6 +586,7 @@
"scene_count": "Scene Count", "scene_count": "Scene Count",
"scene_id": "Scene ID", "scene_id": "Scene ID",
"scenes": "Scenes", "scenes": "Scenes",
"scenes-duration": "Scenes duration",
"scenes-size": "Scenes size", "scenes-size": "Scenes size",
"scenes_updated_at": "Scene Updated At", "scenes_updated_at": "Scene Updated At",
"sceneTagger": "Scene Tagger", "sceneTagger": "Scene Tagger",

View File

@@ -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 formatFileSizeUnit = (u: Unit) => {
const i = Units.indexOf(u); const i = Units.indexOf(u);
return shortUnits[i]; return shortUnits[i];
@@ -206,6 +312,7 @@ const TextUtils = {
instagramURL, instagramURL,
formatDate, formatDate,
capitalize, capitalize,
secondsAsTimeString,
}; };
export default TextUtils; export default TextUtils;