Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7a94b11
chore(server): type media relation for typescript
slugger7 Jun 1, 2026
f95df32
feat(server): create convert data dto
slugger7 Jun 1, 2026
b81662b
chore(server): update scripts to work from root of project
slugger7 Jun 2, 2026
688cfd3
feat(server): create migration to add convert job
slugger7 Jun 2, 2026
30052b6
chore(server): update models
slugger7 Jun 2, 2026
e9caac8
feat(server): create method to determine dimensions
slugger7 Jun 2, 2026
6bf6f6d
feat(server): create job service
slugger7 Jun 2, 2026
53721fb
feat(server): add cache directory option
slugger7 Jun 2, 2026
716f26b
feat(server): setup convert job
slugger7 Jun 3, 2026
ca5a5a6
chore(web): update dtos from backend
slugger7 Jun 3, 2026
a30d5df
feat(server): create convert management
slugger7 Jun 4, 2026
061dd20
feat(server): convert video by scale
slugger7 Jun 4, 2026
19be975
feat(server): add crf and pix_fmt options to convert
slugger7 Jun 4, 2026
84a2275
chore(server): unit tests
slugger7 Jun 4, 2026
1bc9aa1
chore(server): refactor dimension calculations for thumbnail job
slugger7 Jun 4, 2026
aa545f6
feat(web): create button to redirect to conversion page
slugger7 Jun 4, 2026
06d8680
chore(wed): audit fix
slugger7 Jun 4, 2026
5077991
feat(server): create dimension dto
slugger7 Jun 5, 2026
0d6ae21
chore(web): update dtos
slugger7 Jun 5, 2026
72173ad
feat(web): create form for conversion job creation
slugger7 Jun 5, 2026
56554ca
feat(server): update job types
slugger7 Jun 5, 2026
bffec32
chore(web): update dtos
slugger7 Jun 5, 2026
d7cb516
chore(web): refactor job creation uses
slugger7 Jun 5, 2026
d94f8a1
feat(server): properly default dimensions
slugger7 Jun 5, 2026
d136cb8
feat(web): submit convert job
slugger7 Jun 5, 2026
ea2239a
feat(server): copy tags and people on convert
slugger7 Jun 5, 2026
9bd4579
feat(web): add copy people and tags to convert
slugger7 Jun 5, 2026
0a65b92
feat(web): alter dimension if keep scale active
slugger7 Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ SECRET=some-other-super-secret

WEBSOCKET_HEARTBEAT_INTERVAL=15000

CACHE=/cache
ASSETS=/assets
WEB=/web

Expand Down
2 changes: 2 additions & 0 deletions apps/server/internal/db/exorcist/public/enum/job_type_enum.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/server/internal/db/exorcist/public/model/job.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions apps/server/internal/db/exorcist/public/table/job.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions apps/server/internal/dto/helper_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dto

import "github.com/slugger7/exorcist/apps/server/internal/ffmpeg"

type Dimension struct {
Height *int `json:"height"`
Width *int `json:"width"`
}

func (d *Dimension) ToFfmpegDto() *ffmpeg.Dimension {
v := ffmpeg.Dimension{}
if d.Height != nil {
v.Height = new(int)
*v.Height = *d.Height
}

if d.Width != nil {
v.Width = new(int)
*v.Width = *d.Width
}

return &v
}

func (d *Dimension) FromFfmpegDto(m *ffmpeg.Dimension) *Dimension {
if m.Height != nil {
if d.Height == nil {
d.Height = new(int)
}
*d.Height = *m.Height
}

if m.Width != nil {
if d.Width == nil {
d.Width = new(int)
}
*d.Width = *m.Width
}

return d
}
2 changes: 1 addition & 1 deletion apps/server/internal/dto/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

type CreateJobDTO struct {
Type model.JobTypeEnum `json:"type" binding:"required" tstype:"model.JobTypeEnum"`
Data map[string]interface{} `json:"data" tstype:"ScanPathData | GenerateThumbnailData"`
Data map[string]interface{} `json:"data" tstype:"ScanPathData | GenerateThumbnailData | GenerateChaptersData | ConvertData | RefreshMetadata | RefreshLibraryMetadata"`
Priority *JobPriority `json:"priority"`
}

Expand Down
43 changes: 35 additions & 8 deletions apps/server/internal/dto/job_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dto
import (
"github.com/google/uuid"
"github.com/slugger7/exorcist/apps/server/internal/db/exorcist/public/model"
"github.com/slugger7/exorcist/apps/server/internal/ffmpeg"
)

type ScanPathData struct {
Expand All @@ -11,13 +12,11 @@ type ScanPathData struct {

type GenerateThumbnailData struct {
MediaId uuid.UUID `json:"mediaId"`
Path string `json:"path"`
Path string `json:"path" tstype:"-"`
// Optional: If set to 0, timestamp at 25% of video playback will be used. Value in seconds
Timestamp float64 `json:"timestamp"`
// Optional: If set to 0, video height will be used
Height int `json:"height"`
// Optional: If set to 0, video widtch will be used
Width int `json:"width"`
Timestamp float64 `json:"timestamp"`
Height *int `json:"height"`
Width *int `json:"width"`
RelationType *model.MediaRelationTypeEnum `json:"relationType"`
Metadata *ThumbnailMetadataDTO `json:"metadata"`
}
Expand All @@ -41,8 +40,36 @@ type RefreshLibraryMetadata struct {
type GenerateChaptersData struct {
MediaId uuid.UUID `json:"mediaId"`
Interval float64 `json:"interval"`
Height int `json:"height"`
Width int `json:"width"`
Height *int `json:"height"`
Width *int `json:"width"`
MaxDimension int `json:"maxDimension"`
Overwrite bool `json:"overwrite"`
}

type ConvertData struct {
MediaId uuid.UUID `json:"mediaId" binding:"required"`
Dimension Dimension `json:"dimension"`
Filename string `json:"filename" binding:"required"`
CopyTags *bool `json:"copyTags"`
CopyPeople *bool `json:"copyPeople"`
Path string `json:"path" tstype:"-"` // omitted for clients
ConstantRateFactor *int `json:"constantRateFactor"`
VariableBitrate *int `json:"variableBitrate"`
ForcePixelFormat *string `json:"forcePixelFormat"`
}

func (d *ConvertData) ToFfmpegDto() *ffmpeg.ConvertDto {
v := &ffmpeg.ConvertDto{
Dimension: ffmpeg.Dimension{
Height: new(int),
Width: new(int),
},
ConstantRateFactor: d.ConstantRateFactor,
VariableBitrate: d.VariableBitrate,
ForcePixelFormat: d.ForcePixelFormat,
}
*v.Dimension.Height = *d.Dimension.Height
*v.Dimension.Width = *d.Dimension.Width

return v
}
2 changes: 1 addition & 1 deletion apps/server/internal/dto/media_relation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type MediaRelationDto struct {
// to the correct type if needed.
// This is only used to give the client a full json object without them needing
// to parse the json string
Metadata any `json:"metadata"`
Metadata any `json:"metadata" tstype:"ThumbnailMetadataDTO | ChapterMetadadataDTO | null"`
}

func (d *MediaRelationDto) FromModel(m models.MediaRelation) MediaRelationDto {
Expand Down
3 changes: 3 additions & 0 deletions apps/server/internal/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type EnvironmentVariables struct {
Port int
Secret string
LogLevel string
Cache string
Assets string
Web *string
JobRunner bool
Expand All @@ -58,6 +59,7 @@ const (
PORT OsEnv = "PORT"
SECRET OsEnv = "SECRET"
LOG_LEVEL OsEnv = "LOG_LEVEL"
CACHE OsEnv = "CACHE"
ASSETS OsEnv = "ASSETS"
WEB OsEnv = "WEB"
JOB_RUNNER OsEnv = "JOB_RUNNER"
Expand Down Expand Up @@ -94,6 +96,7 @@ func RefreshEnvironmentVariables() {
Port: getIntValue(PORT),
Secret: os.Getenv(SECRET),
LogLevel: getValueOrDefault(LOG_LEVEL, "debug"),
Cache: os.Getenv(CACHE),
Assets: os.Getenv(ASSETS),
Web: getValueOrNil(WEB),
JobRunner: getBoolValue(JOB_RUNNER, true),
Expand Down
49 changes: 49 additions & 0 deletions apps/server/internal/ffmpeg/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ffmpeg

import (
"fmt"
"os"

errs "github.com/slugger7/exorcist/apps/server/internal/errors"
ffmpeg_go "github.com/u2takey/ffmpeg-go"
)

type ConvertDto struct {
InputFilePath string
OutputFilePath string
Dimension Dimension
ConstantRateFactor *int
VariableBitrate *int
ForcePixelFormat *string
}

func Convert(c ConvertDto) error {
if *c.Dimension.Height <= 0 {
return fmt.Errorf(ErrNegativeHeight, *c.Dimension.Height)
}
if *c.Dimension.Width <= 0 {
return fmt.Errorf(ErrNegativeWidth, *c.Dimension.Width)
}

ouptutArgs := ffmpeg_go.KwArgs{"vf": fmt.Sprintf("scale=%v:%v", *c.Dimension.Width, *c.Dimension.Height)}

if c.ConstantRateFactor != nil {
ouptutArgs["crf"] = *c.ConstantRateFactor
}

if c.ForcePixelFormat != nil {
ouptutArgs["pix_fmt"] = *c.ForcePixelFormat
}

err := ffmpeg_go.Input(c.InputFilePath).Output(c.OutputFilePath,
ouptutArgs).
Run()
if err != nil {
str := err.Error()
_ = str
_ = os.Remove(c.OutputFilePath)
return errs.BuildError(err, "error converting %v to %v", c.InputFilePath, c.OutputFilePath)
}

return nil
}
50 changes: 50 additions & 0 deletions apps/server/internal/ffmpeg/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,56 @@ func ScaleHeightByWidth(currentHeight, currentWidth, wantedWidth int) int {
return int(float32(currentHeight) / float32(currentWidth) * float32(wantedWidth))
}

type Dimension struct {
Height *int `json:"height"`
Width *int `json:"width"`
}

func DetermineDimensions(wanted, current Dimension) Dimension {
if wanted.Height != nil && wanted.Width != nil {
return wanted
}

if wanted.Height == nil && wanted.Width == nil {
return current
}

if wanted.Height != nil {
scaledWidth := ScaleWidthByHeight(*current.Height, *current.Width, *wanted.Height)
return Dimension{
Height: wanted.Height,
Width: &scaledWidth,
}
} else {
scaledHeight := ScaleHeightByWidth(*current.Height, *current.Width, *wanted.Width)
return Dimension{
Height: &scaledHeight,
Width: wanted.Width,
}
}
}

func ScaleByMaxDimension(maxDimension int, currentDimension Dimension) *Dimension {
d := Dimension{
Height: new(int),
Width: new(int),
}
*d.Height = *currentDimension.Height
*d.Width = *currentDimension.Width

if *d.Width > maxDimension {
*d.Height = ScaleHeightByWidth(*d.Height, *d.Width, maxDimension)
*d.Width = maxDimension
}

if *d.Height > maxDimension {
*d.Width = ScaleWidthByHeight(*d.Height, *d.Width, maxDimension)
*d.Height = maxDimension
}

return &d
}

func ImageAt(vid string, time float64, img string, width, height int) error {
if width <= 0 {
return fmt.Errorf(ErrNegativeWidth, width)
Expand Down
Loading
Loading