mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add duration statistics to stats page (#1626)
This commit is contained in:
@@ -41,6 +41,7 @@ query Stats {
|
||||
stats {
|
||||
scene_count,
|
||||
scenes_size,
|
||||
scenes_duration,
|
||||
image_count,
|
||||
images_size,
|
||||
gallery_count,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
type StatsResultType {
|
||||
scene_count: Int!
|
||||
scenes_size: Float!
|
||||
scenes_duration: Float!
|
||||
image_count: Int!
|
||||
images_size: Float!
|
||||
gallery_count: Int!
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -39,6 +39,35 @@ export const Stats: React.FC = () => {
|
||||
<FormattedMessage id="scenes" defaultMessage="Scenes" />
|
||||
</p>
|
||||
</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">
|
||||
<p className="title">
|
||||
<FormattedNumber
|
||||
@@ -53,24 +82,6 @@ export const Stats: React.FC = () => {
|
||||
<FormattedMessage id="images-size" defaultMessage="Images size" />
|
||||
</p>
|
||||
</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">
|
||||
<p className="title">
|
||||
<FormattedNumber value={data.stats.gallery_count} />
|
||||
@@ -81,10 +92,10 @@ export const Stats: React.FC = () => {
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
<p className="title">
|
||||
<FormattedNumber value={data.stats.performer_count} />
|
||||
<FormattedNumber value={data.stats.image_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="performers" />
|
||||
<FormattedMessage id="images" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user