Skip to content

Commit

Permalink
Markers can have end time
Browse files Browse the repository at this point in the history
Other metadata sources such as ThePornDB and timestamp.trade support end times for markers but Stash did not yet support saving those. This is a first step which only allows end time to be set either via API or via UI. Other aspects of Stash such as video player timeline are not yet updated to take end time into account.

- User can set end time when creating or editing markers in the UI or in the API.
- End time cannot be before start time. This is validated in the backend and for better UX also in the frontend.
- End time is shown in scene details view or markers wall view if present.
- Existing markers in the database will be updated to have -1 for end.
- GraphQL API does not require end_seconds. Omitted end_seconds will default to -1.
  • Loading branch information
MinasukiHikimuna committed Sep 28, 2024
1 parent cef5b46 commit 57c2d59
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 4 deletions.
3 changes: 3 additions & 0 deletions graphql/schema/types/scene-marker.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ type SceneMarker {
scene: Scene!
title: String!
seconds: Float!
end_seconds: Float!
primary_tag: Tag!
tags: [Tag!]!
created_at: Time!
Expand All @@ -19,6 +20,7 @@ type SceneMarker {
input SceneMarkerCreateInput {
title: String!
seconds: Float!
end_seconds: Float
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
Expand All @@ -28,6 +30,7 @@ input SceneMarkerUpdateInput {
id: ID!
title: String
seconds: Float
end_seconds: Float
scene_id: ID
primary_tag_id: ID
tag_ids: [ID!]
Expand Down
28 changes: 27 additions & 1 deletion internal/api/resolver_mutation_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
newMarker.PrimaryTagID = primaryTagID
newMarker.SceneID = sceneID

if input.EndSeconds != nil {
newMarker.EndSeconds = *input.EndSeconds
// Validate that end_seconds is not less than seconds when it's not -1
if newMarker.EndSeconds != -1 && newMarker.EndSeconds < newMarker.Seconds {
return nil, fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", newMarker.EndSeconds, newMarker.Seconds)
}
} else {
newMarker.EndSeconds = -1
}

tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
Expand Down Expand Up @@ -695,6 +705,9 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar

updatedMarker.Title = translator.optionalString(input.Title, "title")
updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds")
if input.EndSeconds != nil {
updatedMarker.EndSeconds = translator.optionalFloat64(input.EndSeconds, "end_seconds")
}
updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id")
if err != nil {
return nil, fmt.Errorf("converting scene id: %w", err)
Expand Down Expand Up @@ -735,6 +748,19 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
return fmt.Errorf("scene marker with id %d not found", markerID)
}

// Validate end_seconds
newSeconds := existingMarker.Seconds
if updatedMarker.Seconds.Set {
newSeconds = updatedMarker.Seconds.Value
}
newEndSeconds := existingMarker.EndSeconds
if updatedMarker.EndSeconds.Set {
newEndSeconds = updatedMarker.EndSeconds.Value
}
if newEndSeconds != -1 && newEndSeconds < newSeconds {
return fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", newEndSeconds, newSeconds)
}

newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker)
if err != nil {
return err
Expand All @@ -749,7 +775,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
}

// remove the marker preview if the scene changed or if the timestamp was changed
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds {
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
seconds := int(existingMarker.Seconds)
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions pkg/models/model_scene_marker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type SceneMarker struct {
ID int `json:"id"`
Title string `json:"title"`
Seconds float64 `json:"seconds"`
EndSeconds float64 `json:"end_seconds"`
PrimaryTagID int `json:"primary_tag_id"`
SceneID int `json:"scene_id"`
CreatedAt time.Time `json:"created_at"`
Expand All @@ -27,6 +28,7 @@ func NewSceneMarker() SceneMarker {
type SceneMarkerPartial struct {
Title OptionalString
Seconds OptionalFloat64
EndSeconds OptionalFloat64
PrimaryTagID OptionalInt
SceneID OptionalInt
CreatedAt OptionalTime
Expand Down
2 changes: 1 addition & 1 deletion pkg/sqlite/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
)

var appSchemaVersion uint = 67
var appSchemaVersion uint = 68

//go:embed migrations/*.sql
var migrationsBox embed.FS
Expand Down
1 change: 1 addition & 0 deletions pkg/sqlite/migrations/68_markers_end.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE scene_markers ADD COLUMN end_seconds FLOAT NOT NULL DEFAULT -1;
4 changes: 4 additions & 0 deletions pkg/sqlite/scene_marker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ type sceneMarkerRow struct {
SceneID int `db:"scene_id"`
CreatedAt Timestamp `db:"created_at"`
UpdatedAt Timestamp `db:"updated_at"`
EndSeconds float64 `db:"end_seconds"`
}

func (r *sceneMarkerRow) fromSceneMarker(o models.SceneMarker) {
r.ID = o.ID
r.Title = o.Title
r.Seconds = o.Seconds
r.EndSeconds = o.EndSeconds
r.PrimaryTagID = o.PrimaryTagID
r.SceneID = o.SceneID
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
Expand All @@ -48,6 +50,7 @@ func (r *sceneMarkerRow) resolve() *models.SceneMarker {
ID: r.ID,
Title: r.Title,
Seconds: r.Seconds,
EndSeconds: r.EndSeconds,
PrimaryTagID: r.PrimaryTagID,
SceneID: r.SceneID,
CreatedAt: r.CreatedAt.Timestamp,
Expand All @@ -69,6 +72,7 @@ func (r *sceneMarkerRowRecord) fromPartial(o models.SceneMarkerPartial) {
r.set("title", o.Title.Value)
}
r.setFloat64("seconds", o.Seconds)
r.setFloat64("end_seconds", o.EndSeconds)
r.setInt("primary_tag_id", o.PrimaryTagID)
r.setInt("scene_id", o.SceneID)
r.setTimestamp("created_at", o.CreatedAt)
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/graphql/data/scene-marker.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ fragment SceneMarkerData on SceneMarker {
id
title
seconds
end_seconds
stream
preview
screenshot
Expand Down
4 changes: 4 additions & 0 deletions ui/v2.5/graphql/mutations/scene-marker.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mutation SceneMarkerCreate(
$title: String!
$seconds: Float!
$end_seconds: Float!
$scene_id: ID!
$primary_tag_id: ID!
$tag_ids: [ID!] = []
Expand All @@ -9,6 +10,7 @@ mutation SceneMarkerCreate(
input: {
title: $title
seconds: $seconds
end_seconds: $end_seconds
scene_id: $scene_id
primary_tag_id: $primary_tag_id
tag_ids: $tag_ids
Expand All @@ -22,6 +24,7 @@ mutation SceneMarkerUpdate(
$id: ID!
$title: String!
$seconds: Float!
$end_seconds: Float!
$scene_id: ID!
$primary_tag_id: ID!
$tag_ids: [ID!] = []
Expand All @@ -31,6 +34,7 @@ mutation SceneMarkerUpdate(
id: $id
title: $title
seconds: $seconds
end_seconds: $end_seconds
scene_id: $scene_id
primary_tag_id: $primary_tag_id
tag_ids: $tag_ids
Expand Down
8 changes: 7 additions & 1 deletion ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
<FormattedMessage id="actions.edit" />
</Button>
</div>
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
<div>
{marker.end_seconds !== -1
? `${TextUtils.secondsToTimestamp(
marker.seconds
)}-${TextUtils.secondsToTimestamp(marker.end_seconds)}`
: TextUtils.secondsToTimestamp(marker.seconds)}
</div>
<div className="card-section centered">{tags}</div>
</div>
);
Expand Down
45 changes: 45 additions & 0 deletions ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
const schema = yup.object({
title: yup.string().ensure(),
seconds: yup.number().min(0).required(),
end_seconds: yup
.number()
.min(-1)
.test(
"is-greater-than-seconds",
"End time must be greater than or equal to start time",
function (value) {
return (
value !== undefined &&
(value === -1 || value >= this.parent.seconds)
);
}
)
.required(),
primary_tag_id: yup.string().required(),
tag_ids: yup.array(yup.string().required()).defined(),
});
Expand All @@ -53,6 +67,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
() => ({
title: marker?.title ?? "",
seconds: marker?.seconds ?? Math.round(getPlayerPosition() ?? 0),
end_seconds: marker?.end_seconds ?? -1,
primary_tag_id: marker?.primary_tag.id ?? "",
tag_ids: marker?.tags.map((tag) => tag.id) ?? [],
}),
Expand Down Expand Up @@ -205,6 +220,35 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
return renderField("seconds", title, control);
}

function renderEndTimeField() {
const { error } = formik.getFieldMeta("end_seconds");

const title = intl.formatMessage({ id: "time_end" });
const control = (
<>
<DurationInput
value={formik.values.end_seconds}
setValue={(v) => formik.setFieldValue("end_seconds", v)}
onReset={() =>
formik.setFieldValue(
"end_seconds",
Math.round(getPlayerPosition() ?? 0)
)
}
error={error}
allowNegative={true}
/>
{formik.touched.end_seconds && formik.errors.end_seconds && (
<Form.Control.Feedback type="invalid">
{formik.errors.end_seconds}
</Form.Control.Feedback>
)}
</>
);

return renderField("end_seconds", title, control);
}

function renderTagsField() {
const title = intl.formatMessage({ id: "tags" });
const control = (
Expand All @@ -225,6 +269,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
{renderTitleField()}
{renderPrimaryTagField()}
{renderTimeField()}
{renderEndTimeField()}
{renderTagsField()}
</div>
<div className="buttons-container px-3">
Expand Down
7 changes: 6 additions & 1 deletion ui/v2.5/src/components/Wall/WallItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,12 @@ export const WallItem = <T extends WallItemType>({
case "sceneMarker":
const sceneMarker = data as GQL.SceneMarkerDataFragment;
const newTitle = markerTitle(sceneMarker);
const seconds = TextUtils.secondsToTimestamp(sceneMarker.seconds);
const seconds =
sceneMarker.end_seconds !== -1
? `${TextUtils.secondsToTimestamp(
sceneMarker.seconds
)}-${TextUtils.secondsToTimestamp(sceneMarker.end_seconds)}`
: TextUtils.secondsToTimestamp(sceneMarker.seconds);
if (newTitle) {
return `${newTitle} - ${seconds}`;
} else {
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,7 @@
"tags": "Tags",
"tattoos": "Tattoos",
"time": "Time",
"time_end": "Time End",
"title": "Title",
"toast": {
"added_entity": "Added {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
Expand Down

0 comments on commit 57c2d59

Please sign in to comment.