mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add duration statistics to stats page (#1626)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user