├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── discord.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── auth ├── jwt.go └── jwt_stream.go ├── build └── build.sh ├── cloud-config.yaml ├── cmd ├── Config.go ├── CreateUser.go ├── DeleteUser.go ├── ServeMain.go └── init.go ├── config └── config.go ├── configdb └── configdb.go ├── controllers ├── AuthApikeyController.go ├── AuthCheckController.go ├── AuthLoginController.go ├── AuthRefreshController.go ├── CloneFileController.go ├── CreateFileController.go ├── CreateFolderController.go ├── CreateTagController.go ├── CreateUploadChunckController.go ├── CreateUploadFileController.go ├── CreateUploadSessionController.go ├── CreateWebPageController.go ├── CreateWebhookController.go ├── DeleteFileController.go ├── DeleteFilesController.go ├── DeleteFolderController.go ├── DeleteFoldersController.go ├── DeleteTagController.go ├── DeleteUploadSessionController.go ├── DeleteWebPageController.go ├── DeleteWebhookController.go ├── DownloadVideoController.go ├── GetAccountController.go ├── GetAudioDataController.go ├── GetConfigController.go ├── GetEncodingFilesController.go ├── GetFileController.go ├── GetFileExampleController.go ├── GetM3u8DataController.go ├── GetPublicWebPageController.go ├── GetSettingsController.go ├── GetSubtitleDataController.go ├── GetSystemStatsController.go ├── GetThumbnailDataController.go ├── GetUploadSessionsController.go ├── GetUserSettingsController.go ├── GetUsers.go ├── GetVideoDataController.go ├── ListFilesController.go ├── ListFoldersController.go ├── ListPublicWebPageController.go ├── ListWebPageController copy.go ├── ListWebhooksController.go ├── PlayerController.go ├── UpdateFileController.go ├── UpdateFolderController.go ├── UpdateSettingsController.go ├── UpdateUserSettingsController.go ├── UpdateWebPageController.go ├── UpdateWebhookController.go ├── ViewExampleUploadController.go └── ViewIndexController.go ├── docs ├── image.png ├── image2.png ├── image3.png ├── image4.png ├── image5.png ├── image6.png ├── image7.png ├── image8.png └── image9.png ├── go.mod ├── go.sum ├── helpers ├── Captcha.go ├── DirSize.go ├── DynamicJwt.go ├── Folder.go ├── GenM3u8Stream.go ├── GetUser.go ├── HashFile.go ├── LimiterConfig.go ├── LimiterWhitelistIps.go ├── Password.go ├── RemoveFromArray.go ├── UserRequestAsync.go └── Validator.go ├── inits ├── Cache.go ├── Captcha.go ├── Database.go ├── Folders.go ├── Models.go └── Server.go ├── logic ├── CreateFile.go ├── CreateFolder.go ├── CreateTag.go ├── CreateUploadChunck.go ├── CreateUploadFile.go ├── CreateUploadSession.go ├── CreateWebhook.go ├── DeleteFiles.go ├── DeleteFolders.go ├── DeleteTag.go ├── DeleteWebhook.go ├── GetAccount.go ├── GetAudioData.go ├── GetFile.go ├── GetFileExample.go ├── GetM3u8Data.go ├── GetSubtitleData.go ├── GetThumbnailData.go ├── GetVideoData.go ├── HashCloneFile.go ├── ListFiles.go ├── Thumbnail.go └── UpdateWebhook.go ├── main.go ├── middlewares ├── Auth.go ├── IsAdmin.go └── JwtStream.go ├── models ├── Audio.go ├── File.go ├── Folder.go ├── Link.go ├── Model.go ├── Quality.go ├── Setting.go ├── Subtitle.go ├── SystemResource.go ├── Tag.go ├── UploadChunck.go ├── UploadFile.go ├── UploadSession.go ├── User.go ├── UserSettings.go ├── WebPage.go └── Webhooks.go ├── public ├── favicon.ico ├── icons │ └── svgs.svg ├── logo-banner-big.png ├── logo-banner-small.png ├── logo-banner.png └── logo.png ├── routes ├── api.go └── web.go ├── services ├── Deleter.go ├── Encoder.go ├── EncoderCleanup.go └── Resources.go ├── test └── files │ └── test1.mkv ├── tmp └── main └── views ├── 404.html ├── error.html ├── examples └── upload.html ├── index.html ├── player.html └── player.old.html /.dockerignore: -------------------------------------------------------------------------------- 1 | videos 2 | .github 3 | .vscode 4 | test -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | public/js/* linguist-vendored -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/discord.yml: -------------------------------------------------------------------------------- 1 | name: Discord 2 | 3 | on: 4 | - push 5 | jobs: 6 | disord_test_message: 7 | runs-on: ubuntu-latest 8 | name: discord commits 9 | steps: 10 | - name: Discord Commits 11 | uses: Sniddl/discord-commits@v1.6 12 | with: 13 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 14 | template: 'plain-author' 15 | include-extras: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.sqlite 3 | .env 4 | *_bin 5 | videos/* 6 | build/cmd 7 | build/svelte 8 | __debug_bin* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Verwendet IntelliSense zum Ermitteln möglicher Attribute. 3 | // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. 4 | // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "main.go", 13 | "args": [ 14 | "serve:main" 15 | ] 16 | // "env": { 17 | // "ReloadHtml": "true", 18 | // "EncodingEnabled": "true", 19 | // "UploadEnabled": "true", 20 | // "MaxRunningEncodes_sub": "4", 21 | // "MaxRunningEncodes_audio": "4", 22 | // "MaxUploadSessions": "999", 23 | // } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS stage 2 | 3 | WORKDIR /build 4 | 5 | COPY . . 6 | RUN go mod tidy 7 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "-linkmode external -extldflags -static" -a -installsuffix cgo -o main_linux_amd64.bin main.go 8 | RUN sha256sum main_linux_amd64.bin > main_linux_amd64.bin.sha256sum 9 | 10 | FROM alpine:latest 11 | 12 | WORKDIR /app 13 | VOLUME /app/videos 14 | VOLUME /app/public 15 | VOLUME /app/database 16 | 17 | RUN apk add ffmpeg bash 18 | COPY --from=stage ./build/main_linux_amd64.bin ./ 19 | RUN mv ./main_linux_amd64.bin ./main.bin 20 | COPY ./views ./views/ 21 | COPY ./public ./public/ 22 | 23 | ENV Host=:3000 24 | ENV FolderVideoQualitysPriv=./videos/qualitys 25 | ENV FolderVideoQualitysPub=/videos/qualitys 26 | ENV FolderVideoUploadsPriv=./videos/uploads 27 | ENV StatsDriveName=nvme0n1 28 | 29 | EXPOSE 3000 30 | 31 | CMD ["./main.bin", "serve:main"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video-CMS 2 | 3 | This project is a cms for hosting your videos. 4 | 5 | ## Documentation 6 | 7 | Follow the documentation to setup VideoCMS: [https://videocms-docs.vercel.app/](https://videocms-docs.vercel.app/) 8 | 9 | ## Screenshots 10 | 11 | ### Simple Panel 12 |  13 | 14 | ### Advanced File Information 15 |  16 |  17 | 18 | ### Easy Export 19 |  20 |  21 | 22 | ### Multiple Qualities 23 |  24 | 25 | ### Multiple Subtitles 26 |  27 | 28 | ### Multiple Audio Channels 29 |  30 | 31 | ### Embed in Chats (like Discord) 32 |  33 | 34 | ## Build 35 | 36 | ```bash 37 | docker build --platform linux/amd64 -t kirari04/videocms:alpha --push . 38 | ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | -------- | ------------------ | 7 | | latest | :white_check_mark: | 8 | | < latest | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Reproduce the vulnerability and document it in some form. (video or markdown) 13 | -------------------------------------------------------------------------------- /auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | type Claims struct { 14 | UserID uint `json:"userid"` 15 | Username string `json:"username"` 16 | Admin bool `json:"admin"` 17 | jwt.RegisteredClaims 18 | } 19 | 20 | var jwtKey []byte 21 | var sessionDuration = time.Hour * 24 * 7 22 | 23 | func GenerateJWT(user models.User) (string, time.Time, error) { 24 | expirationTime := time.Now().Add(sessionDuration) 25 | return GenerateTimeJWT(user, expirationTime) 26 | } 27 | 28 | func GenerateTimeJWT(user models.User, expirationTime time.Time) (string, time.Time, error) { 29 | jwtKey = []byte(config.ENV.JwtSecretKey) 30 | // Create the JWT claims, which includes the username and expiry time 31 | claims := &Claims{ 32 | UserID: user.ID, 33 | Username: user.Username, 34 | Admin: user.Admin, 35 | RegisteredClaims: jwt.RegisteredClaims{ 36 | // In JWT, the expiry time is expressed as unix milliseconds 37 | ExpiresAt: jwt.NewNumericDate(expirationTime), 38 | }, 39 | } 40 | 41 | // Declare the token with the algorithm used for signing, and the claims 42 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 43 | // Create the JWT string 44 | tokenString, err := token.SignedString(jwtKey) 45 | if err != nil { 46 | return "", time.Now(), err 47 | } 48 | return tokenString, expirationTime, nil 49 | } 50 | 51 | func VerifyJWT(tknStr string) (*jwt.Token, *Claims, error) { 52 | jwtKey = []byte(config.ENV.JwtSecretKey) 53 | claims := &Claims{} 54 | 55 | // Parse the JWT string and store the result in `claims`. 56 | // Note that we are passing the key in this method as well. This method will return an error 57 | // if the token is invalid (if it has expired according to the expiry time we set on sign in), 58 | // or if the signature does not match 59 | tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 60 | return jwtKey, nil 61 | }) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | return tkn, claims, nil 66 | } 67 | 68 | func RefreshJWT(tknStr string) (string, time.Time, error) { 69 | jwtKey = []byte(config.ENV.JwtSecretKey) 70 | claims := &Claims{} 71 | tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 72 | return jwtKey, nil 73 | }) 74 | if err != nil { 75 | return "", time.Now(), errors.New("Malformated jwt key") 76 | } 77 | if !tkn.Valid { 78 | return "", time.Now(), errors.New("Invalid jwt key") 79 | } 80 | 81 | // We ensure that a new token is not issued until enough time has elapsed 82 | // In this case, a new token will only be issued if the old token is within 83 | // 30 seconds of expiry. Otherwise, return a bad request status 84 | if time.Until(claims.ExpiresAt.Time) > 5*time.Minute { 85 | return "", time.Now(), errors.New(fmt.Sprintf("Wait until time to expire: %v", claims.ExpiresAt.Time.String())) 86 | } 87 | 88 | // Now, create a n ew token for the current use, with a renewed expiration time 89 | expirationTime := time.Now().Add(sessionDuration) 90 | claims.ExpiresAt = jwt.NewNumericDate(expirationTime) 91 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 92 | tokenString, err := token.SignedString(jwtKey) 93 | if err != nil { 94 | return "", time.Now(), err 95 | } 96 | 97 | return tokenString, expirationTime, nil 98 | } 99 | -------------------------------------------------------------------------------- /auth/jwt_stream.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt/v5" 9 | ) 10 | 11 | type ClaimsStream struct { 12 | UUID string `json:"uuid"` 13 | jwt.RegisteredClaims 14 | } 15 | 16 | var jwtKeyStream []byte 17 | 18 | func GenerateJWTStream(linkUuid string) (string, time.Time, error) { 19 | jwtKeyStream = []byte(fmt.Sprint(config.ENV.JwtSecretKey, "-stream")) 20 | // Declare the expiration time of the token 21 | // here, we have kept it as 5 minutes 22 | expirationTime := time.Now().Add(24 * time.Hour) 23 | // Create the JWT claims, which includes the username and expiry time 24 | claims := &ClaimsStream{ 25 | UUID: linkUuid, 26 | RegisteredClaims: jwt.RegisteredClaims{ 27 | // In JWT, the expiry time is expressed as unix milliseconds 28 | ExpiresAt: jwt.NewNumericDate(expirationTime), 29 | }, 30 | } 31 | 32 | // Declare the token with the algorithm used for signing, and the claims 33 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 34 | // Create the JWT string 35 | tokenString, err := token.SignedString(jwtKey) 36 | if err != nil { 37 | return "", time.Now(), err 38 | } 39 | return tokenString, expirationTime, nil 40 | } 41 | 42 | func VerifyJWTStream(tknStr string) (*jwt.Token, *ClaimsStream, error) { 43 | jwtKeyStream = []byte(fmt.Sprint(config.ENV.JwtSecretKey, "-stream")) 44 | claims := &ClaimsStream{} 45 | 46 | // Parse the JWT string and store the result in `claims`. 47 | // Note that we are passing the key in this method as well. This method will return an error 48 | // if the token is invalid (if it has expired according to the expiry time we set on sign in), 49 | // or if the signature does not matchas expire 50 | tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 51 | return jwtKey, nil 52 | }) 53 | if err != nil { 54 | return nil, nil, err 55 | } 56 | return tkn, claims, nil 57 | } 58 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # DOCKER 4 | export DOCKER_BUILDKIT=1 5 | docker build . --platform linux/amd64 -f Dockerfile -t kirari04/videocms:alpha --push -------------------------------------------------------------------------------- /cloud-config.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | package_update: true 3 | packages: 4 | - docker.io 5 | 6 | runcmd: 7 | - docker run -d -p 80:3000 -e EncodingEnabled=true -e UploadEnabled=true -e RatelimitEnabled=false kirari04/videocms:panel 8 | - docker volume create portainer_data 9 | - docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest -------------------------------------------------------------------------------- /cmd/Config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | func Config() { 10 | Init() 11 | 12 | // for setting up configuration file from env 13 | config.Setup() 14 | 15 | res2B, _ := json.MarshalIndent(config.ENV, "", " ") 16 | fmt.Println(string(res2B)) 17 | } 18 | -------------------------------------------------------------------------------- /cmd/CreateUser.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "fmt" 9 | "os" 10 | "strings" 11 | "syscall" 12 | 13 | "golang.org/x/term" 14 | ) 15 | 16 | func CreateUser() { 17 | Init() 18 | 19 | reader := bufio.NewReader(os.Stdin) 20 | 21 | fmt.Print("Enter Username: ") 22 | username, err := reader.ReadString('\n') 23 | if err != nil { 24 | fmt.Println(err) 25 | os.Exit(1) 26 | } 27 | 28 | fmt.Print("Enter Password: ") 29 | bytePassword, err := term.ReadPassword(int(syscall.Stdin)) 30 | if err != nil { 31 | fmt.Println(err) 32 | os.Exit(1) 33 | } 34 | 35 | fmt.Print("\nEnter IsAdmin[yes|no]: ") 36 | isAdminRaw, err := reader.ReadString('\n') 37 | if err != nil { 38 | fmt.Println(err) 39 | os.Exit(1) 40 | } 41 | 42 | password := string(bytePassword) 43 | username = strings.TrimSpace(username) 44 | isAdminRaw = strings.TrimSpace(isAdminRaw) 45 | 46 | if isAdminRaw != "yes" && isAdminRaw != "no" { 47 | fmt.Println("invalid input IsAdmin: ", isAdminRaw) 48 | os.Exit(1) 49 | } 50 | var isAdmin bool 51 | if isAdminRaw == "yes" { 52 | isAdmin = true 53 | } 54 | 55 | hash, _ := helpers.HashPassword(password) 56 | user := models.User{ 57 | Username: username, 58 | Hash: hash, 59 | Admin: isAdmin, 60 | Settings: models.UserSettings{ 61 | WebhooksEnabled: true, 62 | WebhooksMax: 10, 63 | }, 64 | } 65 | if res := inits.DB.Create(&user); res.Error != nil { 66 | fmt.Printf("error while creating admin user: %s\n", res.Error.Error()) 67 | os.Exit(1) 68 | } 69 | 70 | fmt.Println("Created user: ", user.ID, user.Username) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/DeleteUser.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func DeleteUser() { 13 | Init() 14 | 15 | reader := bufio.NewReader(os.Stdin) 16 | fmt.Print("Enter Username: ") 17 | username, err := reader.ReadString('\n') 18 | if err != nil { 19 | fmt.Println(err) 20 | os.Exit(1) 21 | } 22 | 23 | username = strings.TrimSpace(username) 24 | 25 | if res := inits.DB. 26 | Where(&models.User{ 27 | Username: username, 28 | }). 29 | Delete(&models.User{}); res.Error != nil { 30 | fmt.Printf("error while deleting admin user: %s\n", res.Error.Error()) 31 | os.Exit(1) 32 | } 33 | 34 | fmt.Println("Deleted User: ", username) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/ServeMain.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/routes" 8 | "ch/kirari04/videocms/services" 9 | ) 10 | 11 | func ServeMain() { 12 | Init() 13 | 14 | // sync UserRequestAsync 15 | helpers.UserRequestAsyncObj.Sync(true) 16 | 17 | // start encoding process 18 | if *config.ENV.EncodingEnabled { 19 | 20 | services.ResetEncodingState() 21 | go services.Encoder() 22 | } 23 | 24 | // start cleanup process 25 | go services.EncoderCleanup() 26 | go services.Deleter() 27 | 28 | // start system resource tracker 29 | go services.Resources() 30 | 31 | // for setting up the webserver 32 | inits.Server() 33 | 34 | // for loading the webservers routes 35 | routes.Api() 36 | routes.Web() 37 | 38 | // for starting the webserver 39 | inits.ServerStart() 40 | } 41 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/configdb" 6 | "ch/kirari04/videocms/helpers" 7 | "ch/kirari04/videocms/inits" 8 | "log" 9 | "os" 10 | ) 11 | 12 | func Init() { 13 | // for setting up configuration file from env 14 | config.Setup() 15 | // setting up required folders and config files 16 | inits.Folders() 17 | // checking env 18 | if errors := helpers.ValidateStruct(config.ENV); len(errors) > 0 { 19 | log.Println("Invalid Env configuration;") 20 | for _, err := range errors { 21 | log.Printf("%v", err) 22 | } 23 | os.Exit(1) 24 | } 25 | // for setting up the database connection 26 | inits.Database() 27 | // for migrating all the models 28 | inits.Models() 29 | // for setting up configuration from db 30 | configdb.Setup() 31 | //setup captcha 32 | inits.Captcha() 33 | } 34 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | Host string `validate:"required,min=1,max=120"` 11 | 12 | AppName string 13 | BaseUrl string 14 | 15 | Project string 16 | ProjectDocumentation string 17 | ProjectDownload string 18 | ProjectExampleVideo string 19 | 20 | JwtSecretKey string 21 | JwtUploadSecretKey string 22 | 23 | ReloadHtml *bool 24 | EncodingEnabled *bool 25 | UploadEnabled *bool 26 | RatelimitEnabled *bool 27 | CloudflareEnabled *bool 28 | 29 | MaxItemsMultiDelete int64 30 | MaxRunningEncodes int64 31 | 32 | MaxUploadFilesize int64 33 | MaxUploadChuncksize int64 34 | MaxUploadSessions int64 35 | MaxPostSize int64 36 | 37 | FolderVideoQualitysPub string `validate:"required,min=1,max=255"` 38 | FolderVideoQualitysPriv string `validate:"required,min=1,max=255"` 39 | FolderVideoUploadsPriv string `validate:"required,min=1,max=255"` 40 | 41 | CorsAllowOrigins string 42 | CorsAllowHeaders string 43 | CorsAllowCredentials *bool 44 | 45 | CaptchaEnabled *bool 46 | CaptchaType string 47 | Captcha_Recaptcha_PrivateKey string 48 | Captcha_Recaptcha_PublicKey string 49 | Captcha_Hcaptcha_PrivateKey string 50 | Captcha_Hcaptcha_PublicKey string 51 | 52 | EncodeHls240p *bool 53 | Hls240pVideoBitrate string 54 | EncodeHls360p *bool 55 | Hls360pVideoBitrate string 56 | EncodeHls480p *bool 57 | Hls480pVideoBitrate string 58 | EncodeHls720p *bool 59 | Hls720pVideoBitrate string 60 | EncodeHls1080p *bool 61 | Hls1080pVideoBitrate string 62 | EncodeHls1440p *bool 63 | Hls1440pVideoBitrate string 64 | EncodeHls2160p *bool 65 | Hls2160pVideoBitrate string 66 | 67 | PluginPgsServer string 68 | EnablePluginPgsServer *bool 69 | 70 | StatsDriveName string `validate:"required,min=1,max=255"` 71 | 72 | DownloadEnabled *bool 73 | } 74 | 75 | type PublicConfig struct { 76 | AppName string 77 | BaseUrl string 78 | Project string 79 | EncodingEnabled bool 80 | UploadEnabled bool 81 | 82 | MaxUploadFilesize int64 83 | MaxUploadChuncksize int64 84 | MaxUploadSessions int64 85 | 86 | FolderVideoQualitys string 87 | 88 | CaptchaEnabled bool 89 | CaptchaType string 90 | Captcha_Recaptcha_PublicKey string 91 | Captcha_Hcaptcha_PublicKey string 92 | 93 | DownloadEnabled bool 94 | } 95 | 96 | func (c Config) PublicConfig() PublicConfig { 97 | return PublicConfig{ 98 | AppName: c.AppName, 99 | BaseUrl: c.BaseUrl, 100 | Project: c.Project, 101 | EncodingEnabled: *c.EncodingEnabled, 102 | UploadEnabled: *c.UploadEnabled, 103 | 104 | MaxUploadFilesize: c.MaxUploadFilesize, 105 | MaxUploadChuncksize: c.MaxUploadChuncksize, 106 | MaxUploadSessions: c.MaxUploadSessions, 107 | 108 | FolderVideoQualitys: c.FolderVideoQualitysPub, 109 | 110 | CaptchaEnabled: *c.CaptchaEnabled, 111 | CaptchaType: c.CaptchaType, 112 | Captcha_Recaptcha_PublicKey: c.Captcha_Recaptcha_PublicKey, 113 | Captcha_Hcaptcha_PublicKey: c.Captcha_Hcaptcha_PublicKey, 114 | 115 | DownloadEnabled: *c.DownloadEnabled, 116 | } 117 | } 118 | 119 | type ConfigMap map[string]string 120 | 121 | var ENV Config 122 | var EXTENSIONS []string = []string{ 123 | "mp4", "mkv", "webm", "avi", "mov", "ts", 124 | } 125 | 126 | func Setup() { 127 | ENV.Host = getEnv("Host", ":3000") 128 | 129 | ENV.FolderVideoQualitysPriv = getEnv("FolderVideoQualitysPriv", "./videos/qualitys") 130 | ENV.FolderVideoQualitysPub = getEnv("FolderVideoQualitysPub", "/videos/qualitys") 131 | ENV.FolderVideoUploadsPriv = getEnv("FolderVideoUploadsPriv", "./videos/uploads") 132 | ENV.StatsDriveName = getEnv("StatsDriveName", "nvme0n1") 133 | } 134 | 135 | // getters 136 | func getEnv(key string, defaultValue string) string { 137 | if value := os.Getenv(key); value != "" { 138 | return value 139 | } 140 | 141 | return defaultValue 142 | } 143 | 144 | func getEnv_bool(key string, defaultValue *bool) *bool { 145 | if value := os.Getenv(key); value != "" { 146 | switch value { 147 | case "true": 148 | return boolPtr(true) 149 | case "1": 150 | return boolPtr(true) 151 | case "false": 152 | return boolPtr(false) 153 | case "0": 154 | return boolPtr(false) 155 | default: 156 | log.Panicf("Failed to get bool from value: %v", value) 157 | } 158 | } 159 | 160 | return defaultValue 161 | } 162 | 163 | func getEnv_int64(key string, defaultValue int64) int64 { 164 | if value := os.Getenv(key); value != "" { 165 | res, err := strconv.ParseInt(value, 10, 64) 166 | if err != nil { 167 | log.Panicf("Failed to parse int from value %v", value) 168 | } 169 | return res 170 | } 171 | 172 | return defaultValue 173 | } 174 | func getEnv_int(key string, defaultValue int) int { 175 | if value := os.Getenv(key); value != "" { 176 | res, err := strconv.Atoi(value) 177 | if err != nil { 178 | log.Panicf("Failed to parse int from value %v", value) 179 | } 180 | return res 181 | } 182 | 183 | return defaultValue 184 | } 185 | 186 | func boolPtr(boolean bool) *bool { 187 | return &boolean 188 | } 189 | -------------------------------------------------------------------------------- /controllers/AuthApikeyController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func AuthApikey(c echo.Context) error { 15 | userId, ok := c.Get("UserID").(uint) 16 | if !ok { 17 | c.Logger().Error("Failed to catch user") 18 | return c.NoContent(http.StatusInternalServerError) 19 | } 20 | 21 | var user models.User 22 | res := inits.DB. 23 | Model(&models.User{}). 24 | First(&user, userId) 25 | if res.Error != nil { 26 | return c.String(http.StatusBadRequest, "User not found") 27 | } 28 | expirationTime := time.Now().Add(time.Hour * 24 * 365) 29 | tokenString, _, err := auth.GenerateTimeJWT(user, expirationTime) 30 | if err != nil { 31 | log.Printf("Failed to generate jwt for user %s: %v\n", user.Username, err) 32 | return c.NoContent(http.StatusInternalServerError) 33 | } 34 | 35 | return c.JSON(http.StatusOK, echo.Map{ 36 | "exp": expirationTime, 37 | "token": tokenString, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /controllers/AuthCheckController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func AuthCheck(c echo.Context) error { 12 | bearer := c.Request().Header.Get("Authorization") 13 | if bearer == "" { 14 | return c.NoContent(http.StatusForbidden) 15 | } 16 | bearerHeader := strings.Split(bearer, " ") 17 | tokenString := bearerHeader[len(bearerHeader)-1] 18 | token, claims, err := auth.VerifyJWT(tokenString) 19 | if err != nil { 20 | return c.NoContent(http.StatusForbidden) 21 | } 22 | if !token.Valid { 23 | return c.NoContent(http.StatusForbidden) 24 | } 25 | return c.JSON(http.StatusOK, echo.Map{ 26 | "username": claims.Username, 27 | "exp": claims.ExpiresAt.Time, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /controllers/AuthLoginController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func AuthLogin(c echo.Context) error { 16 | var userValidation models.UserLoginValidation 17 | if status, err := helpers.Validate(c, &userValidation); err != nil { 18 | return c.String(status, err.Error()) 19 | } 20 | 21 | // validate captcha 22 | success, err := helpers.CaptchaValid(c) 23 | if err != nil { 24 | return c.String(http.StatusBadRequest, fmt.Sprint("Captcha error: ", err.Error())) 25 | } 26 | if !success { 27 | return c.String(http.StatusBadRequest, "Captcha incorrect") 28 | } 29 | 30 | var user models.User 31 | res := inits.DB.Model(&models.User{}).Where(&models.User{ 32 | Username: userValidation.Username, 33 | }).First(&user) 34 | if res.Error != nil { 35 | return c.String(http.StatusBadRequest, "User not found") 36 | } 37 | 38 | if !helpers.CheckPasswordHash(userValidation.Password, user.Hash) { 39 | return c.String(http.StatusBadRequest, "Wrong password") 40 | } 41 | 42 | tokenString, expirationTime, err := auth.GenerateJWT(user) 43 | if err != nil { 44 | log.Printf("Failed to generate jwt for user %s: %v\n", user.Username, err) 45 | return c.NoContent(http.StatusInternalServerError) 46 | } 47 | 48 | return c.JSON(http.StatusOK, echo.Map{ 49 | "exp": expirationTime, 50 | "token": tokenString, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /controllers/AuthRefreshController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func AuthRefresh(c echo.Context) error { 12 | bearer := c.Request().Header.Get("Authorization") 13 | if bearer == "" { 14 | return c.NoContent(http.StatusForbidden) 15 | } 16 | bearerHeader := strings.Split(bearer, " ") 17 | tokenString := bearerHeader[len(bearerHeader)-1] 18 | newTokenString, expirationTime, err := auth.RefreshJWT(tokenString) 19 | if err != nil { 20 | return c.String(http.StatusForbidden, err.Error()) 21 | } 22 | 23 | return c.JSON(http.StatusOK, echo.Map{ 24 | "exp": expirationTime, 25 | "token": newTokenString, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /controllers/CloneFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CloneFile(c echo.Context) error { 12 | // parse & validate request 13 | var fileValidation models.FileCloneValidation 14 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // business logic 19 | status, dbLink, err := logic.CloneFileByHash(fileValidation.Sha256, fileValidation.ParentFolderID, fileValidation.Name, c.Get("UserID").(uint)) 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | 24 | return c.JSON(status, dbLink) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/CreateFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/logic" 7 | "ch/kirari04/videocms/models" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "os" 12 | "strings" 13 | 14 | "github.com/google/uuid" 15 | "github.com/labstack/echo/v4" 16 | ) 17 | 18 | func CreateFile(c echo.Context) error { 19 | if !*config.ENV.UploadEnabled { 20 | return c.String(http.StatusServiceUnavailable, "Upload has been desabled") 21 | } 22 | 23 | // parse & validate request 24 | var fileValidation models.FileCreateValidation 25 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 26 | return c.String(status, err.Error()) 27 | } 28 | 29 | file, err := c.FormFile("file") 30 | if err != nil { 31 | return c.String(http.StatusBadRequest, "No File uploaded") 32 | } 33 | src, err := file.Open() 34 | if err != nil { 35 | c.Logger().Error("Failed to open src file", err) 36 | return c.NoContent(http.StatusInternalServerError) 37 | } 38 | defer src.Close() 39 | 40 | fileId := uuid.NewString() 41 | fileSplit := strings.Split(file.Filename, ".") 42 | fileExt := fileSplit[len(fileSplit)-1] 43 | filePath := fmt.Sprintf("%s/%s.%s", config.ENV.FolderVideoUploadsPriv, fileId, fileExt) 44 | 45 | // Save file to storage 46 | dst, err := os.Create(filePath) 47 | if err != nil { 48 | c.Logger().Error("Failed to open destination file", err) 49 | return c.NoContent(http.StatusInternalServerError) 50 | } 51 | defer dst.Close() 52 | if _, err = io.Copy(dst, src); err != nil { 53 | c.Logger().Errorf("Failed to save file: %v", err) 54 | return c.NoContent(http.StatusInternalServerError) 55 | } 56 | 57 | // business logic 58 | status, dbLink, cloned, err := logic.CreateFile(&filePath, fileValidation.ParentFolderID, file.Filename, fileId, file.Size, c.Get("UserID").(uint)) 59 | if err != nil || cloned { 60 | os.Remove(filePath) 61 | } 62 | if err != nil { 63 | return c.String(status, err.Error()) 64 | } 65 | 66 | return c.JSON(status, dbLink) 67 | } 68 | -------------------------------------------------------------------------------- /controllers/CreateFolderController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CreateFolder(c echo.Context) error { 12 | // parse & validate request 13 | var folderValidation models.FolderCreateValidation 14 | if status, err := helpers.Validate(c, &folderValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, dbFolder, err := logic.CreateFolder(folderValidation.Name, folderValidation.ParentFolderID, c.Get("UserID").(uint)) 19 | 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | 24 | return c.JSON(status, dbFolder) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/CreateTagController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CreateTagController(c echo.Context) error { 12 | // parse & validate request 13 | var validator models.TagCreateValidation 14 | if status, err := helpers.Validate(c, &validator); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, dbTag, err := logic.CreateTag(validator.Name, validator.LinkId, c.Get("UserID").(uint)) 19 | 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | 24 | return c.JSON(status, dbTag) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/CreateUploadChunckController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/logic" 7 | "ch/kirari04/videocms/models" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "os" 12 | "strings" 13 | 14 | "github.com/google/uuid" 15 | "github.com/labstack/echo/v4" 16 | ) 17 | 18 | // this route is not securet with user jwt token so it doesnt invalidate the chunck because the session invalidated during the upload time 19 | func CreateUploadChunck(c echo.Context) error { 20 | // parse & validate request 21 | var validation models.UploadChunckValidation 22 | if status, err := helpers.Validate(c, &validation); err != nil { 23 | return c.String(status, err.Error()) 24 | } 25 | 26 | // file validation 27 | file, err := c.FormFile("file") 28 | if err != nil { 29 | return c.String(http.StatusBadRequest, "No File uploaded") 30 | } 31 | src, err := file.Open() 32 | if err != nil { 33 | c.Logger().Error("Failed to open src file", err) 34 | return c.NoContent(http.StatusInternalServerError) 35 | } 36 | defer src.Close() 37 | 38 | fileId := uuid.NewString() 39 | fileSplit := strings.Split(file.Filename, ".") 40 | fileExt := fileSplit[len(fileSplit)-1] 41 | filePath := fmt.Sprintf("%s/%s.%s", config.ENV.FolderVideoUploadsPriv, fileId, fileExt) 42 | 43 | // Save file to storage 44 | dst, err := os.Create(filePath) 45 | if err != nil { 46 | c.Logger().Error("Failed to open destination file", err) 47 | return c.NoContent(http.StatusInternalServerError) 48 | } 49 | defer dst.Close() 50 | if _, err = io.Copy(dst, src); err != nil { 51 | c.Logger().Errorf("Failed to save file: %v", err) 52 | return c.NoContent(http.StatusInternalServerError) 53 | } 54 | 55 | // business logic 56 | status, response, err := logic.CreateUploadChunck(*validation.Index, validation.SessionJwtToken, filePath) 57 | if err != nil { 58 | os.Remove(filePath) 59 | return c.String(status, err.Error()) 60 | } 61 | 62 | return c.JSON(status, response) 63 | } 64 | -------------------------------------------------------------------------------- /controllers/CreateUploadFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CreateUploadFile(c echo.Context) error { 12 | // parse & validate request 13 | var validation models.UploadFileValidation 14 | if status, err := helpers.Validate(c, &validation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, response, err := logic.CreateUploadFile( 19 | validation.SessionJwtToken, 20 | c.Get("UserID").(uint), 21 | ) 22 | if err != nil { 23 | return c.String(status, err.Error()) 24 | } 25 | 26 | return c.JSON(status, response) 27 | } 28 | -------------------------------------------------------------------------------- /controllers/CreateUploadSessionController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/google/uuid" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func CreateUploadSession(c echo.Context) error { 13 | // parse & validate request 14 | var validation models.UploadSessionValidation 15 | if status, err := helpers.Validate(c, &validation); err != nil { 16 | return c.String(status, err.Error()) 17 | } 18 | 19 | // business logic 20 | uploadSessionUUID := uuid.NewString() 21 | status, response, err := logic.CreateUploadSession( 22 | validation.ParentFolderID, 23 | validation.Name, 24 | uploadSessionUUID, 25 | validation.Size, 26 | c.Get("UserID").(uint), 27 | ) 28 | if err != nil { 29 | return c.String(status, err.Error()) 30 | } 31 | 32 | return c.JSON(status, response) 33 | } 34 | -------------------------------------------------------------------------------- /controllers/CreateWebPageController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func CreateWebPage(c echo.Context) error { 15 | // parse & validate request 16 | var validatus models.WebPageCreateValidation 17 | if status, err := helpers.Validate(c, &validatus); err != nil { 18 | return c.String(status, err.Error()) 19 | } 20 | 21 | var existing int64 22 | if res := inits.DB.Model(&models.WebPage{}).Where(&models.WebPage{ 23 | Path: validatus.Path, 24 | }).Count(&existing); res.Error != nil { 25 | c.Logger().Error("Failed to count webpage path", res.Error) 26 | return c.NoContent(http.StatusInternalServerError) 27 | } 28 | if existing > 0 { 29 | return c.String(http.StatusBadRequest, "Path already used") 30 | } 31 | 32 | if validatus.Path[len(validatus.Path)-1] != '/' { 33 | validatus.Path = fmt.Sprintf("%s/", validatus.Path) 34 | } 35 | 36 | webPage := models.WebPage{ 37 | Path: validatus.Path, 38 | Title: validatus.Title, 39 | Html: validatus.Html, 40 | ListInFooter: *validatus.ListInFooter, 41 | } 42 | if res := inits.DB.Create(&webPage); res.Error != nil { 43 | log.Println("Failed to create webpage", res.Error) 44 | return c.NoContent(http.StatusInternalServerError) 45 | } 46 | 47 | return c.String(http.StatusOK, "ok") 48 | } 49 | -------------------------------------------------------------------------------- /controllers/CreateWebhookController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func CreateWebhook(c echo.Context) error { 12 | // parse & validate request 13 | 14 | var webhookValidation models.WebhookCreateValidation 15 | if status, err := helpers.Validate(c, &webhookValidation); err != nil { 16 | return c.String(status, err.Error()) 17 | } 18 | 19 | status, res, err := logic.CreateWebhook(&webhookValidation, c.Get("UserID").(uint)) 20 | 21 | if err != nil { 22 | return c.String(status, err.Error()) 23 | } 24 | 25 | return c.String(status, res) 26 | } 27 | -------------------------------------------------------------------------------- /controllers/DeleteFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteFileController(c echo.Context) error { 12 | // parse & validate request 13 | var fileValidation models.LinkDeleteValidation 14 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // Business logic 19 | status, err := logic.DeleteFiles(&models.LinksDeleteValidation{ 20 | LinkIDs: []models.LinkDeleteValidation{ 21 | fileValidation, 22 | }, 23 | }, c.Get("UserID").(uint)) 24 | 25 | if err != nil { 26 | return c.String(status, err.Error()) 27 | } 28 | return c.NoContent(status) 29 | } 30 | -------------------------------------------------------------------------------- /controllers/DeleteFilesController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteFilesController(c echo.Context) error { 12 | // parse & validate request 13 | var fileValidation models.LinksDeleteValidation 14 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // Business logic 19 | status, err := logic.DeleteFiles(&fileValidation, c.Get("UserID").(uint)) 20 | 21 | if err != nil { 22 | return c.String(status, err.Error()) 23 | } 24 | return c.NoContent(status) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/DeleteFolderController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteFolder(c echo.Context) error { 12 | // parse & validate request 13 | var folderValidation models.FolderDeleteValidation 14 | if status, err := helpers.Validate(c, &folderValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // Business logic 19 | status, err := logic.DeleteFolders(&models.FoldersDeleteValidation{ 20 | FolderIDs: []models.FolderDeleteValidation{ 21 | folderValidation, 22 | }, 23 | }, c.Get("UserID").(uint)) 24 | if err != nil { 25 | return c.String(status, err.Error()) 26 | } 27 | return c.NoContent(status) 28 | } 29 | -------------------------------------------------------------------------------- /controllers/DeleteFoldersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteFolders(c echo.Context) error { 12 | // parse & validate request 13 | var folderValidation models.FoldersDeleteValidation 14 | if status, err := helpers.Validate(c, &folderValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // Business logic 19 | status, err := logic.DeleteFolders(&folderValidation, c.Get("UserID").(uint)) 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | return c.NoContent(status) 24 | } 25 | -------------------------------------------------------------------------------- /controllers/DeleteTagController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteTagController(c echo.Context) error { 12 | // parse & validate request 13 | var validator models.TagDeleteValidation 14 | if status, err := helpers.Validate(c, &validator); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, err := logic.DeleteTag(validator.TagId, validator.LinkId, c.Get("UserID").(uint)) 19 | 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | 24 | return c.NoContent(status) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/DeleteUploadSessionController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func DeleteUploadSession(c echo.Context) error { 15 | // parse & validate request 16 | var validation models.DeleteUploadSessionValidation 17 | if status, err := helpers.Validate(c, &validation); err != nil { 18 | return c.String(status, err.Error()) 19 | } 20 | 21 | userId, ok := c.Get("UserID").(uint) 22 | if !ok { 23 | log.Println("GetUploadSessions: Failed to catch userId") 24 | return c.NoContent(http.StatusInternalServerError) 25 | } 26 | 27 | var uploadSession models.UploadSession 28 | if res := inits.DB.Where(&models.UploadSession{ 29 | UUID: validation.UploadSessionUUID, 30 | }, "UUID").First(&uploadSession); res.Error != nil { 31 | return c.String(http.StatusBadRequest, "Upload Session not found") 32 | } 33 | 34 | if uploadSession.UserID != userId { 35 | return c.String(http.StatusBadRequest, "Upload Session not found") 36 | } 37 | 38 | if res := inits.DB. 39 | Model(&models.UploadChunck{}). 40 | Where(&models.UploadChunck{ 41 | UploadSessionID: uploadSession.ID, 42 | }). 43 | Delete(&models.UploadChunck{}); res.Error != nil { 44 | log.Printf("[WARNING] createUploadFileCleanup -> remove upload chuncks from database (%d): %v\n", uploadSession.ID, res.Error) 45 | } 46 | if res := inits.DB. 47 | Delete(&models.UploadSession{}, uploadSession.ID); res.Error != nil { 48 | log.Printf("[WARNING] createUploadFileCleanup -> remove upload session from database (%d): %v\n", uploadSession.ID, res.Error) 49 | } 50 | 51 | if err := os.RemoveAll(uploadSession.SessionFolder); err != nil { 52 | log.Printf("[WARNING] createUploadFileCleanup -> remove session folder: %v\n", err) 53 | } 54 | 55 | return c.String(http.StatusOK, "ok") 56 | } 57 | -------------------------------------------------------------------------------- /controllers/DeleteWebPageController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func DeleteWebPage(c echo.Context) error { 14 | // parse & validate request 15 | var validatus models.WebPageDeleteValidation 16 | if status, err := helpers.Validate(c, &validatus); err != nil { 17 | return c.String(status, err.Error()) 18 | } 19 | 20 | res := inits.DB.Delete(&models.WebPage{}, validatus.WebPageID) 21 | if res.Error != nil { 22 | log.Println("Failed to delete webpage", res.Error) 23 | return c.NoContent(http.StatusInternalServerError) 24 | } 25 | if res.RowsAffected <= 0 { 26 | return c.String(http.StatusBadRequest, "Webpage not found") 27 | } 28 | 29 | return c.String(http.StatusOK, "ok") 30 | } 31 | -------------------------------------------------------------------------------- /controllers/DeleteWebhookController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func DeleteWebhook(c echo.Context) error { 12 | // parse & validate request 13 | var validation models.WebhookDeleteValidation 14 | if status, err := helpers.Validate(c, &validation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | userID := c.Get("UserID").(uint) 19 | 20 | status, response, err := logic.DeleteWebhook(&validation, userID) 21 | if err != nil { 22 | return c.String(status, err.Error()) 23 | } 24 | 25 | return c.String(status, response) 26 | } 27 | -------------------------------------------------------------------------------- /controllers/DownloadVideoController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "fmt" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "time" 14 | 15 | "github.com/google/uuid" 16 | "github.com/labstack/echo/v4" 17 | ) 18 | 19 | func DownloadVideoController(c echo.Context) error { 20 | type Request struct { 21 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 22 | QUALITY string `validate:"required,min=1,max=10" param:"QUALITY"` 23 | Stream *bool `validate:"omitempty,boolean" param:"STREAM"` 24 | } 25 | var requestValidation Request 26 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 27 | return c.String(status, err.Error()) 28 | } 29 | 30 | if requestValidation.Stream == nil { 31 | requestValidation.Stream = new(bool) 32 | } 33 | 34 | reQUALITY := regexp.MustCompile(`^([0-9]{3,4}p|(h264))$`) 35 | if !reQUALITY.MatchString(requestValidation.QUALITY) { 36 | return c.String(http.StatusBadRequest, "bad quality format") 37 | } 38 | 39 | if config.ENV.DownloadEnabled == nil || !*config.ENV.DownloadEnabled { 40 | return c.String(http.StatusBadRequest, "download disabled") 41 | } 42 | 43 | //translate link id to file id 44 | var dbLink models.Link 45 | if dbRes := inits.DB. 46 | Model(&models.Link{}). 47 | Preload("File"). 48 | Preload("File.Subtitles"). 49 | Preload("File.Audios"). 50 | Preload("File.Qualitys"). 51 | Where(&models.Link{ 52 | UUID: requestValidation.UUID, 53 | }). 54 | First(&dbLink); dbRes.Error != nil { 55 | return c.String(http.StatusBadRequest, "video doesn't exist") 56 | } 57 | files := []string{} 58 | streamIndex := 0 59 | 60 | if !*requestValidation.Stream { 61 | // add subtitles 62 | for _, subtitle := range dbLink.File.Subtitles { 63 | files = append(files, "-i", fmt.Sprintf( 64 | "%s/%s", 65 | subtitle.Path, 66 | subtitle.OutputFile, 67 | )) 68 | streamIndex++ 69 | } 70 | } 71 | 72 | // add audios 73 | if !*requestValidation.Stream { 74 | for _, audio := range dbLink.File.Audios { 75 | files = append(files, "-i", fmt.Sprintf( 76 | "%s/%s", 77 | audio.Path, 78 | audio.OutputFile, 79 | )) 80 | streamIndex++ 81 | } 82 | } else { 83 | if len(dbLink.File.Audios) > 0 { 84 | files = append(files, "-i", fmt.Sprintf( 85 | "%s/%s", 86 | dbLink.File.Audios[0].Path, 87 | dbLink.File.Audios[0].OutputFile, 88 | )) 89 | streamIndex++ 90 | } 91 | } 92 | 93 | // add video 94 | for _, quality := range dbLink.File.Qualitys { 95 | if quality.Name == requestValidation.QUALITY { 96 | files = append(files, "-i", fmt.Sprintf( 97 | "%s/%s", 98 | quality.Path, 99 | quality.OutputFile, 100 | )) 101 | streamIndex++ 102 | } 103 | } 104 | 105 | for i := 0; i < streamIndex; i++ { 106 | files = append(files, "-map", fmt.Sprintf("%d", i)) 107 | } 108 | 109 | tmpFilePath := fmt.Sprintf("%s/%s-tmp-enc.mp4", config.ENV.FolderVideoUploadsPriv, uuid.NewString()) 110 | defer os.Remove(tmpFilePath) 111 | var cmdString []string 112 | if !*requestValidation.Stream { 113 | cmdString = append(files, []string{"-c", "copy", "-f", "matroska", tmpFilePath}...) 114 | } else { 115 | cmdString = append(files, []string{"-c", "copy", "-f", "mp4", tmpFilePath}...) 116 | } 117 | cmd := exec.Command("ffmpeg", cmdString...) 118 | 119 | if err := cmd.Start(); err != nil { 120 | c.Logger().Error("Failed to run cmd", err) 121 | return nil 122 | } 123 | 124 | if err := cmd.Wait(); err != nil { 125 | c.Logger().Error("Failed to run cmd on wait", err) 126 | return nil 127 | } 128 | 129 | // wait until file exists 130 | var tmpFile *os.File 131 | var fileName string 132 | if *requestValidation.Stream { 133 | f, err := os.Open(tmpFilePath) 134 | if err != nil { 135 | c.Logger().Error("Failed to open tmp file", err) 136 | return nil 137 | } 138 | tmpFile = f 139 | fileName = fmt.Sprintf( 140 | "%s[%s].mp4", 141 | regexp.MustCompile(`[^a-zA-Z0-9]+`).ReplaceAllString(dbLink.Name, "-"), 142 | requestValidation.QUALITY, 143 | ) 144 | } else { 145 | f, err := os.Open(tmpFilePath) 146 | if err != nil { 147 | c.Logger().Error("Failed to open tmp file", err) 148 | return nil 149 | } 150 | tmpFile = f 151 | fileName = fmt.Sprintf( 152 | "%s[%s].mkv", 153 | regexp.MustCompile(`[^a-zA-Z0-9]+`).ReplaceAllString(dbLink.Name, "-"), 154 | requestValidation.QUALITY, 155 | ) 156 | } 157 | defer tmpFile.Close() 158 | 159 | if !*requestValidation.Stream { 160 | defer os.Remove(tmpFilePath) 161 | c.Response().Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName)) 162 | return c.Stream(http.StatusOK, "video/x-matroska", tmpFile) 163 | } else { 164 | c.Response().Header().Add("Accept-Ranges", "bytes") 165 | http.ServeContent(c.Response(), c.Request(), fileName, time.Now(), tmpFile) 166 | defer os.Remove(tmpFilePath) 167 | return nil 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /controllers/GetAccountController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/logic" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func GetAccount(c echo.Context) error { 10 | status, dbAccount, err := logic.GetAccount(c.Get("UserID").(uint)) 11 | if err != nil { 12 | return c.String(status, err.Error()) 13 | } 14 | 15 | return c.JSON(status, dbAccount) 16 | } 17 | -------------------------------------------------------------------------------- /controllers/GetAudioDataController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func GetAudioData(c echo.Context) error { 13 | var requestValidation models.AudioGetValidation 14 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, filePath, err := logic.GetAudioData(&requestValidation) 19 | if err != nil { 20 | return c.String(status, err.Error()) 21 | } 22 | 23 | if err := c.File(*filePath); err != nil { 24 | return c.String(http.StatusNotFound, "Audio doesn't exist") 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /controllers/GetConfigController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func GetConfig(c echo.Context) error { 11 | return c.JSON(http.StatusOK, config.ENV.PublicConfig()) 12 | } 13 | -------------------------------------------------------------------------------- /controllers/GetEncodingFilesController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | type GetEncodingFilesRes struct { 12 | ID uint 13 | Name string 14 | Quality string 15 | Progress float64 16 | } 17 | 18 | func GetEncodingFiles(c echo.Context) error { 19 | userId, ok := c.Get("UserID").(uint) 20 | if !ok { 21 | c.Logger().Error("Failed to catch user") 22 | return c.NoContent(http.StatusInternalServerError) 23 | } 24 | var res []GetEncodingFilesRes 25 | 26 | var resQuality []GetEncodingFilesRes 27 | if err := inits.DB. 28 | Model(&models.Link{}). 29 | Select( 30 | "links.id as id", 31 | "links.name as name", 32 | "qualities.name as quality", 33 | "qualities.progress as progress", 34 | ). 35 | Where("links.user_id = ?", userId). 36 | Joins("JOIN files ON files.id = links.file_id"). 37 | Joins("JOIN qualities ON files.id = qualities.file_id AND qualities.failed = ? AND qualities.ready = ?", false, false). 38 | Order("files.id ASC"). 39 | Scan(&resQuality).Error; err != nil { 40 | c.Logger().Error("Failed to list encoding files", err) 41 | return c.NoContent(http.StatusInternalServerError) 42 | } 43 | 44 | var resAudio []GetEncodingFilesRes 45 | if err := inits.DB. 46 | Model(&models.Link{}). 47 | Select( 48 | "links.id as id", 49 | "links.name as name", 50 | "audios.name as quality", 51 | "audios.progress as progress", 52 | ). 53 | Where("links.user_id = ?", userId). 54 | Joins("JOIN files ON files.id = links.file_id"). 55 | Joins("JOIN audios ON files.id = audios.file_id AND audios.failed = ? AND audios.ready = ?", false, false). 56 | Order("files.id ASC"). 57 | Scan(&resAudio).Error; err != nil { 58 | c.Logger().Error("Failed to list encoding files", err) 59 | return c.NoContent(http.StatusInternalServerError) 60 | } 61 | 62 | var resSubtitle []GetEncodingFilesRes 63 | if err := inits.DB. 64 | Model(&models.Link{}). 65 | Select( 66 | "links.id as id", 67 | "links.name as name", 68 | "subtitles.name as quality", 69 | "subtitles.progress as progress", 70 | ). 71 | Where("links.user_id = ?", userId). 72 | Joins("JOIN files ON files.id = links.file_id"). 73 | Joins("JOIN subtitles ON files.id = subtitles.file_id AND subtitles.failed = ? AND subtitles.ready = ?", false, false). 74 | Order("files.id ASC"). 75 | Scan(&resSubtitle).Error; err != nil { 76 | c.Logger().Error("Failed to list encoding files", err) 77 | return c.NoContent(http.StatusInternalServerError) 78 | } 79 | 80 | res = append(res, resSubtitle...) 81 | res = append(res, resAudio...) 82 | res = append(res, resQuality...) 83 | 84 | return c.JSON(http.StatusOK, &res) 85 | } 86 | -------------------------------------------------------------------------------- /controllers/GetFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func GetFile(c echo.Context) error { 12 | // parse & validate request 13 | var fileValidation models.LinkGetValidation 14 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | // Business logic 19 | status, response, err := logic.GetFile(fileValidation.LinkID, c.Get("UserID").(uint)) 20 | if err != nil { 21 | return c.String(status, err.Error()) 22 | } 23 | 24 | return c.JSON(status, response) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/GetFileExampleController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/logic" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func GetFileExample(c echo.Context) error { 10 | status, response, err := logic.GetFileExample() 11 | if err != nil { 12 | return c.String(status, err.Error()) 13 | } 14 | return c.String(status, response) 15 | } 16 | -------------------------------------------------------------------------------- /controllers/GetM3u8DataController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func GetM3u8Data(c echo.Context) error { 13 | var requestValidation logic.GetM3u8DataRequest 14 | var requestValidationMuted logic.GetM3u8DataRequestMuted 15 | requestValidation.UUID = c.Param("UUID") 16 | requestValidation.AUDIOUUID = c.Param("AUDIOUUID") 17 | requestValidation.JWT = c.QueryParam("jwt") 18 | var JWT string 19 | if requestValidation.AUDIOUUID != "" { 20 | // validate audio stream 21 | if errors := helpers.ValidateStruct(requestValidation); len(errors) > 0 { 22 | return c.String(http.StatusBadRequest, fmt.Sprintf("%s [%s] : %s", errors[0].FailedField, errors[0].Tag, errors[0].Value)) 23 | } 24 | JWT = requestValidation.JWT 25 | } else { 26 | // validate muted stream 27 | requestValidationMuted.UUID = requestValidation.UUID 28 | requestValidationMuted.JWT = requestValidation.JWT 29 | if errors := helpers.ValidateStruct(requestValidationMuted); len(errors) > 0 { 30 | return c.String(http.StatusBadRequest, fmt.Sprintf("%s [%s] : %s", errors[0].FailedField, errors[0].Tag, errors[0].Value)) 31 | } 32 | JWT = requestValidationMuted.JWT 33 | } 34 | 35 | status, m3u8Str, err := logic.GetM3u8Data(requestValidation.UUID, requestValidation.AUDIOUUID, JWT) 36 | if err != nil { 37 | return c.String(status, err.Error()) 38 | } 39 | 40 | return c.String(status, *m3u8Str) 41 | } 42 | -------------------------------------------------------------------------------- /controllers/GetPublicWebPageController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func GetPublicWebPage(c echo.Context) error { 15 | // parse & validate request 16 | var validatus models.WebPageGetValidation 17 | if status, err := helpers.Validate(c, &validatus); err != nil { 18 | return c.String(status, err.Error()) 19 | } 20 | 21 | var webPage models.WebPage 22 | if res := inits.DB. 23 | Where(&models.WebPage{ 24 | Path: validatus.Path, 25 | }). 26 | First(&webPage); res.Error != nil { 27 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 28 | return c.String(http.StatusNotFound, "Page not found") 29 | } 30 | c.Logger().Error("Failed to get webpage", res.Error) 31 | return c.NoContent(http.StatusInternalServerError) 32 | } 33 | 34 | return c.String(http.StatusOK, webPage.Html) 35 | } 36 | -------------------------------------------------------------------------------- /controllers/GetSettingsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func GetSettings(c echo.Context) error { 13 | _, ok := c.Get("UserID").(uint) 14 | if !ok { 15 | log.Println("Failed to catch user") 16 | return c.NoContent(http.StatusInternalServerError) 17 | } 18 | 19 | var setting models.Setting 20 | if res := inits.DB.FirstOrCreate(&setting); res.Error != nil { 21 | log.Fatalln("Failed to get settings", res.Error) 22 | return c.NoContent(http.StatusInternalServerError) 23 | } 24 | return c.JSON(http.StatusOK, &setting) 25 | } 26 | -------------------------------------------------------------------------------- /controllers/GetSubtitleDataController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func GetSubtitleData(c echo.Context) error { 12 | type Request struct { 13 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 14 | SUBUUID string `validate:"required,uuid_rfc4122" param:"SUBUUID"` 15 | FILE string `validate:"required" param:"FILE"` 16 | } 17 | var requestValidation Request 18 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 19 | return c.String(status, err.Error()) 20 | } 21 | 22 | status, filePath, err := logic.GetSubtitleData(requestValidation.FILE, requestValidation.UUID, requestValidation.SUBUUID) 23 | if err != nil { 24 | return c.String(status, err.Error()) 25 | } 26 | 27 | if err := c.File(*filePath); err != nil { 28 | return c.String(http.StatusNotFound, "Subtitle file not found") 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /controllers/GetSystemStatsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | type StatItem struct { 15 | CreatedAt time.Time 16 | Cpu float64 17 | Mem float64 18 | NetOut float64 19 | NetIn float64 20 | DiskW float64 21 | DiskR float64 22 | ENCQualityQueue float64 23 | ENCAudioQueue float64 24 | ENCSubtitleQueue float64 25 | } 26 | 27 | func GetSystemStats(c echo.Context) error { 28 | var validatus models.SystemResourceGetValidation 29 | if status, err := helpers.Validate(c, &validatus); err != nil { 30 | return c.String(status, err.Error()) 31 | } 32 | 33 | amount := 48 34 | duration := time.Minute * 5 35 | if validatus.Interval == "5min" { 36 | amount = 48 37 | duration = time.Minute * 5 38 | } 39 | if validatus.Interval == "1h" { 40 | amount = 24 41 | duration = time.Hour 42 | } 43 | if validatus.Interval == "7h" { 44 | amount = 24 45 | duration = time.Hour * 7 46 | } 47 | var response []StatItem 48 | for i := 0; i < amount; i++ { 49 | var resources StatItem 50 | addFrom := duration * time.Duration(amount-(i)) * -1 51 | from := time.Now().Add(addFrom) 52 | addUntil := duration * time.Duration(amount-(i+1)) * -1 53 | until := time.Now().Add(addUntil) 54 | if res := inits.DB. 55 | Model(&models.SystemResource{}). 56 | Select( 57 | "created_at as created_at", 58 | "AVG(cpu) as cpu", 59 | "AVG(mem) as mem", 60 | "AVG(net_out) as net_out", 61 | "AVG(net_in) as net_in", 62 | "AVG(disk_w) as disk_w", 63 | "AVG(disk_r) as disk_ru", 64 | "AVG(enc_quality_queue) as enc_quality_queue", 65 | "AVG(enc_audio_queue) as enc_audio_queue", 66 | "AVG(enc_subtitle_queue) as enc_subtitle_queue", 67 | ). 68 | Where("created_at > ?", from). 69 | Where("created_at < ?", until). 70 | Where("server_id IS NULL"). 71 | Find(&resources); res.Error != nil { 72 | log.Println("Failed to query stats", res.Error) 73 | return c.NoContent(http.StatusInternalServerError) 74 | } 75 | response = append(response, StatItem{ 76 | CreatedAt: time.Now().Add(duration * time.Duration(amount-(i+1)) * -1), 77 | Cpu: resources.Cpu, 78 | Mem: resources.Mem, 79 | NetOut: resources.NetOut, 80 | NetIn: resources.NetIn, 81 | DiskW: resources.DiskW, 82 | DiskR: resources.DiskR, 83 | ENCQualityQueue: resources.ENCQualityQueue, 84 | ENCAudioQueue: resources.ENCAudioQueue, 85 | ENCSubtitleQueue: resources.ENCSubtitleQueue, 86 | }) 87 | } 88 | 89 | return c.JSON(http.StatusOK, &response) 90 | } 91 | -------------------------------------------------------------------------------- /controllers/GetThumbnailDataController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func GetThumbnailData(c echo.Context) error { 12 | type Request struct { 13 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 14 | FILE string `validate:"required" param:"FILE"` 15 | } 16 | var requestValidation Request 17 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 18 | return c.String(status, err.Error()) 19 | } 20 | 21 | _, filePath, err := logic.GetThumbnailData(requestValidation.FILE, requestValidation.UUID) 22 | if err != nil { 23 | return c.NoContent(http.StatusNotFound) 24 | } 25 | 26 | if err := c.File(*filePath); err != nil { 27 | return c.NoContent(http.StatusNotFound) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /controllers/GetUploadSessionsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | type GetUploadSessionsRes struct { 14 | CreatedAt *time.Time 15 | Name string 16 | UUID string 17 | Size int64 18 | ChunckCount int 19 | } 20 | 21 | func GetUploadSessions(c echo.Context) error { 22 | userId, ok := c.Get("UserID").(uint) 23 | if !ok { 24 | log.Println("GetUploadSessions: Failed to catch userId") 25 | return c.NoContent(http.StatusInternalServerError) 26 | } 27 | 28 | var sessions []GetUploadSessionsRes 29 | if res := inits.DB. 30 | Model(&models.UploadSession{}). 31 | Where(&models.UploadSession{ 32 | UserID: userId, 33 | }, "UserID"). 34 | Find(&sessions); res.Error != nil { 35 | log.Println("Failed to list upload sessions", res.Error) 36 | return c.NoContent(http.StatusInternalServerError) 37 | } 38 | 39 | return c.JSON(http.StatusOK, &sessions) 40 | } 41 | -------------------------------------------------------------------------------- /controllers/GetUserSettingsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func GetUserSettingsController(c echo.Context) error { 13 | userId, ok := c.Get("UserID").(uint) 14 | if !ok { 15 | log.Println("Failed to catch userID") 16 | return c.NoContent(http.StatusInternalServerError) 17 | } 18 | 19 | var user models.User 20 | if res := inits.DB.First(&user, userId); res.Error != nil { 21 | log.Println("Failed to catch userID on db") 22 | return c.NoContent(http.StatusInternalServerError) 23 | } 24 | 25 | return c.JSON(http.StatusOK, echo.Map{ 26 | "EnablePlayerCaptcha": user.Settings.EnablePlayerCaptcha, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /controllers/GetUsers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func GetUsers(c echo.Context) error { 13 | users := make([]models.User, 0) 14 | if res := inits.DB.First(&users); res.Error != nil { 15 | log.Println("Failed to fetch users") 16 | return c.NoContent(http.StatusInternalServerError) 17 | } 18 | 19 | return c.JSON(http.StatusOK, &users) 20 | } 21 | -------------------------------------------------------------------------------- /controllers/GetVideoDataController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func GetVideoData(c echo.Context) error { 12 | type Request struct { 13 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 14 | QUALITY string `validate:"required,min=1,max=10" param:"QUALITY"` 15 | FILE string `validate:"required" param:"FILE"` 16 | } 17 | var requestValidation Request 18 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 19 | return c.String(status, err.Error()) 20 | } 21 | 22 | status, filePath, err := logic.GetVideoData(requestValidation.FILE, requestValidation.QUALITY, requestValidation.UUID) 23 | if err != nil { 24 | return c.String(status, err.Error()) 25 | } 26 | 27 | if err := c.File(*filePath); err != nil { 28 | return c.String(http.StatusNotFound, "Video doesn't exist") 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /controllers/ListFilesController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func ListFiles(c echo.Context) error { 12 | // parse & validate request 13 | var fileValidation models.LinkListValidation 14 | if status, err := helpers.Validate(c, &fileValidation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | status, response, err := logic.ListFiles(fileValidation.ParentFolderID, c.Get("UserID").(uint)) 19 | if err != nil { 20 | return c.String(status, err.Error()) 21 | } 22 | 23 | return c.JSON(status, response) 24 | } 25 | -------------------------------------------------------------------------------- /controllers/ListFoldersController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func ListFolders(c echo.Context) error { 14 | // parse & validate request 15 | var folderValidation models.FolderListValidation 16 | if status, err := helpers.Validate(c, &folderValidation); err != nil { 17 | return c.String(status, err.Error()) 18 | } 19 | 20 | //check if requested folder exists 21 | if folderValidation.ParentFolderID > 0 { 22 | res := inits.DB.First(&models.Folder{}, folderValidation.ParentFolderID) 23 | if res.Error != nil { 24 | return c.String(http.StatusBadRequest, "Parent folder doesn't exist") 25 | } 26 | } 27 | 28 | // query all folders 29 | var folders []models.Folder 30 | res := inits.DB. 31 | Model(&models.Folder{}). 32 | Preload("User"). 33 | Where(&models.Folder{ 34 | ParentFolderID: folderValidation.ParentFolderID, 35 | UserID: c.Get("UserID").(uint), 36 | }, "ParentFolderID", "UserID"). 37 | Order("name ASC"). 38 | Find(&folders) 39 | if res.Error != nil { 40 | log.Printf("Failed to query folder list: %v", res.Error) 41 | return c.NoContent(http.StatusInternalServerError) 42 | } 43 | 44 | // return value 45 | return c.JSON(http.StatusOK, &folders) 46 | } 47 | -------------------------------------------------------------------------------- /controllers/ListPublicWebPageController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | type listPublicWebPageRes struct { 12 | Path string 13 | Title string 14 | ListInFooter bool 15 | } 16 | 17 | func ListPublicWebPage(c echo.Context) error { 18 | var webPages []listPublicWebPageRes 19 | if res := inits.DB. 20 | Model(&models.WebPage{}). 21 | Select( 22 | "path", 23 | "title", 24 | "list_in_footer", 25 | ). 26 | Find(&webPages); res.Error != nil { 27 | c.Logger().Error("Failed to list webpages", res.Error) 28 | return c.NoContent(http.StatusInternalServerError) 29 | } 30 | 31 | return c.JSON(http.StatusOK, &webPages) 32 | } 33 | -------------------------------------------------------------------------------- /controllers/ListWebPageController copy.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func ListWebPage(c echo.Context) error { 13 | var webPages []models.WebPage 14 | if res := inits.DB.Find(&webPages); res.Error != nil { 15 | log.Println("Failed to list webpages", res.Error) 16 | return c.NoContent(http.StatusInternalServerError) 17 | } 18 | return c.JSON(http.StatusOK, &webPages) 19 | } 20 | -------------------------------------------------------------------------------- /controllers/ListWebhooksController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func ListWebhooks(c echo.Context) error { 14 | // parse & validate request 15 | var validation models.WebhookListValidation 16 | if status, err := helpers.Validate(c, &validation); err != nil { 17 | return c.String(status, err.Error()) 18 | } 19 | 20 | userID := c.Get("UserID").(uint) 21 | 22 | var dataList []models.Webhook 23 | if res := inits.DB.Where(&models.Webhook{ 24 | UserID: userID, 25 | }).Find(&dataList); res.Error != nil { 26 | log.Printf("Failed to fetch webhooks from database: %v", res.Error) 27 | return c.NoContent(http.StatusInternalServerError) 28 | } 29 | 30 | return c.JSON(http.StatusOK, &dataList) 31 | } 32 | -------------------------------------------------------------------------------- /controllers/PlayerController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "ch/kirari04/videocms/config" 6 | "ch/kirari04/videocms/helpers" 7 | "ch/kirari04/videocms/inits" 8 | "ch/kirari04/videocms/models" 9 | "encoding/base64" 10 | "encoding/json" 11 | "fmt" 12 | "html/template" 13 | "log" 14 | "net/http" 15 | "os" 16 | "strconv" 17 | 18 | "github.com/labstack/echo/v4" 19 | ) 20 | 21 | func PlayerController(c echo.Context) error { 22 | // parse & validate request 23 | type Request struct { 24 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 25 | } 26 | var requestValidation Request 27 | if status, err := helpers.Validate(c, &requestValidation); err != nil { 28 | return c.Render(status, "error.html", echo.Map{ 29 | "Title": "Player Error", 30 | "Error": err.Error(), 31 | }) 32 | } 33 | 34 | //check if requested folder exists 35 | var dbLink models.Link 36 | if res := inits.DB. 37 | Preload("File"). 38 | Preload("File.Qualitys"). 39 | Preload("File.Subtitles"). 40 | Preload("File.Audios"). 41 | Where(&models.Link{ 42 | UUID: requestValidation.UUID, 43 | }). 44 | First(&dbLink); res.Error != nil { 45 | return c.Render(http.StatusNotFound, "404.html", echo.Map{}) 46 | } 47 | 48 | // generate jwt token that allows the user to access the stream 49 | tkn, _, err := auth.GenerateJWTStream(dbLink.UUID) 50 | if err != nil { 51 | log.Printf("Failed to generate jwt stream token: %v", err) 52 | return c.NoContent(http.StatusInternalServerError) 53 | } 54 | 55 | // List qualitys non hls & check if has some file is ready 56 | var streamIsReady bool 57 | var jsonQualitys []map[string]string 58 | streamUrl := "" 59 | streamUrlWidth := "" 60 | streamUrlHeight := "" 61 | for _, qualiItem := range dbLink.File.Qualitys { 62 | if qualiItem.Ready { 63 | streamIsReady = true 64 | jsonQualitys = append(jsonQualitys, map[string]string{ 65 | "url": fmt.Sprintf("%s/%s/%s/download/video.mkv?jwt=%s", config.ENV.FolderVideoQualitysPub, dbLink.UUID, qualiItem.Name, tkn), 66 | "label": qualiItem.Name, 67 | "height": strconv.Itoa(int(qualiItem.Height)), 68 | "width": strconv.Itoa(int(qualiItem.Width)), 69 | }) 70 | streamUrl = fmt.Sprintf("%s/%s/%s/%s/1/stream/video.mp4", config.ENV.FolderVideoQualitysPub, dbLink.UUID, qualiItem.Name, tkn) 71 | streamUrlHeight = strconv.Itoa(int(qualiItem.Height)) 72 | streamUrlWidth = strconv.Itoa(int(qualiItem.Width)) 73 | } 74 | } 75 | rawQuality, _ := json.Marshal(jsonQualitys) 76 | 77 | // List subtitles 78 | var jsonSubtitles []map[string]string 79 | for _, subItem := range dbLink.File.Subtitles { 80 | if subItem.Ready { 81 | subPath := fmt.Sprintf("%s/%s/%s/%s", config.ENV.FolderVideoQualitysPriv, dbLink.File.UUID, subItem.UUID, subItem.OutputFile) 82 | if subContent, err := os.ReadFile(subPath); err == nil { 83 | jsonSubtitles = append(jsonSubtitles, map[string]string{ 84 | "data": base64.StdEncoding.EncodeToString(subContent), 85 | "type": subItem.Type, 86 | "name": subItem.Name, 87 | "lang": subItem.Lang, 88 | }) 89 | } 90 | } 91 | } 92 | rawSubtitles, _ := json.Marshal(jsonSubtitles) 93 | 94 | // List audios 95 | var jsonAudios []map[string]string 96 | var firstAudio string 97 | for _, audioItem := range dbLink.File.Audios { 98 | if audioItem.Ready { 99 | jsonAudios = append(jsonAudios, map[string]string{ 100 | "uuid": audioItem.UUID, 101 | "type": audioItem.Type, 102 | "name": audioItem.Name, 103 | "lang": audioItem.Lang, 104 | "file": audioItem.OutputFile, 105 | }) 106 | firstAudio = audioItem.UUID 107 | } 108 | } 109 | rawAudios, _ := json.Marshal(jsonAudios) 110 | 111 | // List webhooks 112 | var webhooks []models.Webhook 113 | if res := inits.DB. 114 | Where(&models.Webhook{ 115 | UserID: dbLink.UserID, 116 | }). 117 | Find(&webhooks); res.Error != nil { 118 | log.Printf("Failed to query webhooks of file owner: %v", res.Error) 119 | return c.NoContent(http.StatusInternalServerError) 120 | } 121 | var jsonWebhooks []map[string]any 122 | for _, webhookItem := range webhooks { 123 | jsonWebhooks = append(jsonWebhooks, map[string]any{ 124 | "url": webhookItem.Url, 125 | "rpm": webhookItem.Rpm, 126 | "reqQuery": webhookItem.ReqQuery, 127 | "resField": webhookItem.ResField, 128 | }) 129 | } 130 | rawWebhooks, _ := json.Marshal(jsonWebhooks) 131 | 132 | // "{{.UUID}}={{.JWT}}; path=/; domain=" + window.location.hostname + ";SameSite=None; Secure; HttpOnly" 133 | // c.SetCookie(&http.Cookie{ 134 | // Name: requestValidation.UUID, 135 | // Value: tkn, 136 | // Path: "/", 137 | // Secure: true, 138 | // SameSite: "None", 139 | // Domain: config.ENV.CookieDomain, 140 | // HTTPOnly: true, 141 | // }) 142 | 143 | var downloadsEnabled bool 144 | if config.ENV.DownloadEnabled != nil { 145 | downloadsEnabled = *config.ENV.DownloadEnabled 146 | } 147 | 148 | return c.Render(http.StatusOK, "player.html", echo.Map{ 149 | "Title": fmt.Sprintf("%s - %s", config.ENV.AppName, dbLink.Name), 150 | "Description": fmt.Sprintf("Watch %s on %s", dbLink.Name, config.ENV.AppName), 151 | "Thumbnail": fmt.Sprintf("%s/%s/image/thumb/%s", config.ENV.FolderVideoQualitysPub, dbLink.UUID, dbLink.File.Thumbnail), 152 | "StreamUrl": template.HTML(streamUrl), 153 | "StreamUrlWidth": streamUrlWidth, 154 | "StreamUrlHeight": streamUrlHeight, 155 | "Width": dbLink.File.Width, 156 | "Height": dbLink.File.Height, 157 | "Qualitys": string(rawQuality), 158 | "Subtitles": string(rawSubtitles), 159 | "Audios": string(rawAudios), 160 | "AudioUUID": firstAudio, 161 | "Webhooks": string(rawWebhooks), 162 | "StreamIsReady": streamIsReady, 163 | "UUID": requestValidation.UUID, 164 | "PROJECTURL": config.ENV.Project, 165 | "Folder": config.ENV.FolderVideoQualitysPub, 166 | "JWT": tkn, 167 | "AppName": config.ENV.AppName, 168 | "BaseUrl": config.ENV.BaseUrl, 169 | "DownloadEnabled": downloadsEnabled, 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /controllers/UpdateFileController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func UpdateFile(c echo.Context) error { 14 | // parse & validate request 15 | var linkValidation models.LinkUpdateValidation 16 | if status, err := helpers.Validate(c, &linkValidation); err != nil { 17 | return c.String(status, err.Error()) 18 | } 19 | 20 | var dbLink models.Link 21 | //check if requested file /link id exists 22 | if res := inits.DB.First(&dbLink, linkValidation.LinkID); res.Error != nil { 23 | return c.String(http.StatusBadRequest, "File doesn't exist") 24 | } 25 | 26 | if linkValidation.ParentFolderID > 0 { 27 | if res := inits.DB.First(&models.Folder{}, linkValidation.ParentFolderID); res.Error != nil { 28 | return c.String(http.StatusBadRequest, "Parent folder doesn't exist") 29 | } 30 | } 31 | 32 | //update link data 33 | dbLink.Name = linkValidation.Name 34 | dbLink.ParentFolderID = linkValidation.ParentFolderID 35 | if res := inits.DB.Save(&dbLink); res.Error != nil { 36 | log.Printf("Failed to update link: %v", res.Error) 37 | return c.NoContent(http.StatusInternalServerError) 38 | } 39 | 40 | return c.NoContent(http.StatusOK) 41 | } 42 | -------------------------------------------------------------------------------- /controllers/UpdateFolderController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | /* 14 | This function shouldnt be runned in concurrency. 15 | The reason for that is, if the user woulf start the process of delete for the root folder 16 | and shortly after calls the delete method for the root folder too, it would try to list the 17 | whole folder tree and delete all folders & files again. To prevent that there is an map variable 18 | that should prevent the user from calling this method multiple times concurrently. 19 | */ 20 | func UpdateFolder(c echo.Context) error { 21 | // parse & validate request 22 | var folderValidation models.FolderUpdateValidation 23 | if status, err := helpers.Validate(c, &folderValidation); err != nil { 24 | return c.String(status, err.Error()) 25 | } 26 | 27 | var dbFolder models.Folder 28 | //check if requested folder id exists 29 | if res := inits.DB.First(&dbFolder, folderValidation.FolderID); res.Error != nil { 30 | return c.String(http.StatusBadRequest, "Folder doesn't exist") 31 | } 32 | 33 | /* 34 | check if ParentfolderID aint root folder (=0) 35 | check if requested parent folder id exists 36 | TODO: also check if the new parent folder is not a child of current folder or the folder itself 37 | */ 38 | if folderValidation.ParentFolderID > 0 { 39 | if res := inits.DB.First(&models.Folder{}, folderValidation.ParentFolderID); res.Error != nil { 40 | return c.String(http.StatusBadRequest, "Parent folder doesn't exist") 41 | } 42 | 43 | // if the new parent folder is inside the current folder we return an 44 | // error so the folders wont be in an infinite loop 45 | containsFolder, err := helpers.FolderContainsFolder(dbFolder.ID, dbFolder.ParentFolderID) 46 | if err != nil { 47 | log.Printf("While running FolderContainsFolder the database returned an error: %v", err) 48 | return c.NoContent(http.StatusInternalServerError) 49 | } 50 | if containsFolder { 51 | return c.String(http.StatusBadRequest, "Parent folder aint a parent folder in relation to new exist") 52 | } 53 | } 54 | 55 | //update folder data 56 | dbFolder.Name = folderValidation.Name 57 | dbFolder.ParentFolderID = folderValidation.ParentFolderID 58 | if res := inits.DB.Save(&dbFolder); res.Error != nil { 59 | log.Printf("Failed to update folder: %v", res.Error) 60 | return c.NoContent(http.StatusInternalServerError) 61 | } 62 | 63 | return c.NoContent(http.StatusOK) 64 | } 65 | -------------------------------------------------------------------------------- /controllers/UpdateSettingsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/configdb" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | func UpdateSettings(c echo.Context) error { 15 | 16 | // parse & validate request 17 | var validation models.SettingValidation 18 | if status, err := helpers.Validate(c, &validation); err != nil { 19 | return c.String(status, err.Error()) 20 | } 21 | 22 | if res := inits.DB.First(&models.Setting{}, validation.ID); res.Error != nil { 23 | return c.String(http.StatusBadRequest, "Setting not found by id") 24 | } 25 | 26 | var setting models.Setting 27 | setting.ID = validation.ID 28 | setting.AppName = validation.AppName 29 | setting.BaseUrl = validation.BaseUrl 30 | setting.Project = validation.Project 31 | setting.ProjectDocumentation = validation.ProjectDocumentation 32 | setting.ProjectDownload = validation.ProjectDownload 33 | setting.ProjectExampleVideo = validation.ProjectExampleVideo 34 | setting.JwtSecretKey = validation.JwtSecretKey 35 | setting.JwtUploadSecretKey = validation.JwtUploadSecretKey 36 | setting.ReloadHtml = validation.ReloadHtml 37 | setting.EncodingEnabled = validation.EncodingEnabled 38 | setting.UploadEnabled = validation.UploadEnabled 39 | setting.RatelimitEnabled = validation.RatelimitEnabled 40 | setting.CloudflareEnabled = validation.CloudflareEnabled 41 | setting.MaxItemsMultiDelete = validation.MaxItemsMultiDelete 42 | setting.MaxRunningEncodes = validation.MaxRunningEncodes 43 | setting.MaxUploadFilesize = validation.MaxUploadFilesize 44 | setting.MaxUploadChuncksize = validation.MaxUploadChuncksize 45 | setting.MaxUploadSessions = validation.MaxUploadSessions 46 | setting.MaxPostSize = validation.MaxPostSize 47 | setting.CorsAllowHeaders = validation.CorsAllowHeaders 48 | setting.CorsAllowOrigins = validation.CorsAllowOrigins 49 | setting.CorsAllowCredentials = validation.CorsAllowCredentials 50 | setting.CaptchaEnabled = validation.CaptchaEnabled 51 | setting.CaptchaType = validation.CaptchaType 52 | setting.Captcha_Recaptcha_PrivateKey = validation.Captcha_Recaptcha_PrivateKey 53 | setting.Captcha_Recaptcha_PublicKey = validation.Captcha_Recaptcha_PublicKey 54 | setting.Captcha_Hcaptcha_PrivateKey = validation.Captcha_Hcaptcha_PrivateKey 55 | setting.Captcha_Hcaptcha_PublicKey = validation.Captcha_Hcaptcha_PublicKey 56 | setting.EncodeHls240p = validation.EncodeHls240p 57 | setting.Hls240pVideoBitrate = validation.Hls240pVideoBitrate 58 | setting.EncodeHls360p = validation.EncodeHls360p 59 | setting.Hls360pVideoBitrate = validation.Hls360pVideoBitrate 60 | setting.EncodeHls480p = validation.EncodeHls480p 61 | setting.Hls480pVideoBitrate = validation.Hls480pVideoBitrate 62 | setting.EncodeHls720p = validation.EncodeHls720p 63 | setting.Hls720pVideoBitrate = validation.Hls720pVideoBitrate 64 | setting.EncodeHls1080p = validation.EncodeHls1080p 65 | setting.Hls1080pVideoBitrate = validation.Hls1080pVideoBitrate 66 | setting.EncodeHls1440p = validation.EncodeHls1440p 67 | setting.Hls1440pVideoBitrate = validation.Hls1440pVideoBitrate 68 | setting.EncodeHls2160p = validation.EncodeHls2160p 69 | setting.Hls2160pVideoBitrate = validation.Hls2160pVideoBitrate 70 | setting.PluginPgsServer = validation.PluginPgsServer 71 | setting.EnablePluginPgsServer = validation.EnablePluginPgsServer 72 | setting.DownloadEnabled = validation.DownloadEnabled 73 | 74 | if res := inits.DB.Save(&setting); res.Error != nil { 75 | log.Fatalln("Failed to save settings", res.Error) 76 | return c.NoContent(http.StatusInternalServerError) 77 | } 78 | // reload config in background 79 | go func() { 80 | configdb.Setup() 81 | log.Println("reloaded config") 82 | }() 83 | return c.String(http.StatusOK, "ok") 84 | } 85 | -------------------------------------------------------------------------------- /controllers/UpdateUserSettingsController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func UpdateUserSettingsController(c echo.Context) error { 14 | // parse & validate request 15 | var validater models.UserSettingsUpdateValidation 16 | if status, err := helpers.Validate(c, &validater); err != nil { 17 | return c.String(status, err.Error()) 18 | } 19 | 20 | userId, ok := c.Get("UserID").(uint) 21 | if !ok { 22 | log.Println("Failed to catch userID") 23 | return c.NoContent(http.StatusInternalServerError) 24 | } 25 | 26 | var user models.User 27 | if res := inits.DB.First(&user, userId); res.Error != nil { 28 | log.Println("Failed to catch userID on db") 29 | return c.NoContent(http.StatusInternalServerError) 30 | } 31 | 32 | user.Settings.EnablePlayerCaptcha = *validater.EnablePlayerCaptcha 33 | 34 | if res := inits.DB.Save(&user); res.Error != nil { 35 | log.Println("Failed to update user settings", res.Error) 36 | return c.NoContent(http.StatusInternalServerError) 37 | } 38 | 39 | return c.NoContent(http.StatusOK) 40 | } 41 | -------------------------------------------------------------------------------- /controllers/UpdateWebPageController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/labstack/echo/v4" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func UpdateWebPage(c echo.Context) error { 16 | // parse & validate request 17 | var validatus models.WebPageUpdateValidation 18 | if status, err := helpers.Validate(c, &validatus); err != nil { 19 | return c.String(status, err.Error()) 20 | } 21 | 22 | var existing int64 23 | if res := inits.DB.Model(&models.WebPage{}). 24 | Where("id != ?", validatus.WebPageID). 25 | Where("path = ?", validatus.Path). 26 | Count(&existing); res.Error != nil { 27 | log.Println("Failed to count webpage path", res.Error) 28 | return c.NoContent(http.StatusInternalServerError) 29 | } 30 | if existing > 0 { 31 | return c.String(http.StatusBadRequest, "Path already used") 32 | } 33 | 34 | var webPage models.WebPage 35 | if res := inits.DB.First(&webPage, validatus.WebPageID); res.Error != nil { 36 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 37 | return c.String(http.StatusNotFound, "Webpage not found") 38 | } 39 | log.Println("Failed to find webpage", res.Error) 40 | return c.NoContent(http.StatusInternalServerError) 41 | } 42 | 43 | webPage.Path = validatus.Path 44 | webPage.Title = validatus.Title 45 | webPage.Html = validatus.Html 46 | webPage.ListInFooter = *validatus.ListInFooter 47 | 48 | if res := inits.DB.Save(&webPage); res.Error != nil { 49 | log.Println("Failed to update webpage", res.Error) 50 | return c.NoContent(http.StatusInternalServerError) 51 | } 52 | 53 | return c.String(http.StatusOK, "ok") 54 | } 55 | -------------------------------------------------------------------------------- /controllers/UpdateWebhookController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/logic" 6 | "ch/kirari04/videocms/models" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func UpdateWebhook(c echo.Context) error { 12 | // parse & validate request 13 | var validation models.WebhookUpdateValidation 14 | if status, err := helpers.Validate(c, &validation); err != nil { 15 | return c.String(status, err.Error()) 16 | } 17 | 18 | userID := c.Get("UserID").(uint) 19 | 20 | status, response, err := logic.UpdateWebhook(&validation, userID) 21 | if err != nil { 22 | return c.String(status, err.Error()) 23 | } 24 | 25 | return c.String(status, response) 26 | } 27 | -------------------------------------------------------------------------------- /controllers/ViewExampleUploadController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func ViewExampleUpload(c echo.Context) error { 11 | return c.Render(http.StatusOK, "examples/upload.html", echo.Map{ 12 | "AppName": config.ENV.AppName, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /controllers/ViewIndexController.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func ViewIndex(c echo.Context) error { 14 | var link models.Link 15 | if res := inits.DB.First(&link); res.Error != nil { 16 | return c.Render(http.StatusOK, "index.html", echo.Map{ 17 | "ExampleVideo": fmt.Sprintf("/%v", "notfound"), 18 | "AppName": config.ENV.AppName, 19 | "ProjectDocumentation": config.ENV.ProjectDocumentation, 20 | "ProjectDownload": config.ENV.ProjectDownload, 21 | }) 22 | } 23 | return c.Render(http.StatusOK, "index.html", echo.Map{ 24 | "ExampleVideo": fmt.Sprintf("/%v", link.UUID), 25 | "AppName": config.ENV.AppName, 26 | "ProjectDocumentation": config.ENV.ProjectDocumentation, 27 | "ProjectDownload": config.ENV.ProjectDownload, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /docs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image.png -------------------------------------------------------------------------------- /docs/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image2.png -------------------------------------------------------------------------------- /docs/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image3.png -------------------------------------------------------------------------------- /docs/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image4.png -------------------------------------------------------------------------------- /docs/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image5.png -------------------------------------------------------------------------------- /docs/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image6.png -------------------------------------------------------------------------------- /docs/image7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image7.png -------------------------------------------------------------------------------- /docs/image8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image8.png -------------------------------------------------------------------------------- /docs/image9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/docs/image9.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ch/kirari04/videocms 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/dpapathanasiou/go-recaptcha v0.0.0-20190121160230-be5090b17804 9 | github.com/go-playground/validator/v10 v10.26.0 10 | github.com/golang-jwt/jwt/v5 v5.2.2 11 | github.com/imroc/req/v3 v3.52.2 12 | github.com/labstack/echo/v4 v4.13.4 13 | github.com/patrickmn/go-cache v2.1.0+incompatible 14 | github.com/thatisuday/commando v1.0.4 15 | golang.org/x/net v0.40.0 16 | golang.org/x/term v0.32.0 17 | golang.org/x/time v0.11.0 18 | gopkg.in/vansante/go-ffprobe.v2 v2.2.1 19 | gorm.io/driver/sqlite v1.5.7 20 | gorm.io/gorm v1.30.0 21 | ) 22 | 23 | require ( 24 | github.com/andybalholm/brotli v1.1.1 // indirect 25 | github.com/cloudflare/circl v1.6.1 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 28 | github.com/go-ole/go-ole v1.3.0 // indirect 29 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 30 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 31 | github.com/hashicorp/errwrap v1.1.0 // indirect 32 | github.com/hashicorp/go-multierror v1.1.1 // indirect 33 | github.com/icholy/digest v1.1.0 // indirect 34 | github.com/klauspost/compress v1.18.0 // indirect 35 | github.com/labstack/gommon v0.4.2 // indirect 36 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 37 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 39 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 40 | github.com/quic-go/qpack v0.5.1 // indirect 41 | github.com/quic-go/quic-go v0.52.0 // indirect 42 | github.com/refraction-networking/utls v1.7.3 // indirect 43 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 44 | github.com/thatisuday/clapper v1.0.10 // indirect 45 | github.com/tklauser/go-sysconf v0.3.15 // indirect 46 | github.com/tklauser/numcpus v0.10.0 // indirect 47 | github.com/valyala/fasttemplate v1.2.2 // indirect 48 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 49 | go.uber.org/automaxprocs v1.6.0 // indirect 50 | go.uber.org/mock v0.5.2 // indirect 51 | golang.org/x/mod v0.24.0 // indirect 52 | golang.org/x/sync v0.14.0 // indirect 53 | golang.org/x/tools v0.33.0 // indirect 54 | ) 55 | 56 | require ( 57 | github.com/go-playground/locales v0.14.1 // indirect 58 | github.com/go-playground/universal-translator v0.18.1 // indirect 59 | github.com/google/uuid v1.6.0 60 | github.com/jinzhu/inflection v1.0.0 // indirect 61 | github.com/jinzhu/now v1.1.5 // indirect 62 | github.com/kirari04/go-hcaptcha v0.0.0-20230322135436-9fe4847aa674 63 | github.com/leodido/go-urn v1.4.0 // indirect 64 | github.com/mattn/go-colorable v0.1.14 // indirect 65 | github.com/mattn/go-isatty v0.0.20 // indirect 66 | github.com/mattn/go-sqlite3 v1.14.28 // indirect 67 | github.com/shirou/gopsutil/v3 v3.24.5 68 | github.com/valyala/bytebufferpool v1.0.0 // indirect 69 | golang.org/x/crypto v0.38.0 70 | golang.org/x/sys v0.33.0 // indirect 71 | golang.org/x/text v0.25.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /helpers/Captcha.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "errors" 6 | 7 | recaptcha "github.com/dpapathanasiou/go-recaptcha" 8 | hcaptcha "github.com/kirari04/go-hcaptcha" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func CaptchaValid(c echo.Context) (bool, error) { 13 | if !*config.ENV.CaptchaEnabled { 14 | return true, nil 15 | } 16 | 17 | switch config.ENV.CaptchaType { 18 | case "recaptcha": 19 | return recaptchaValidate(c) 20 | case "hcaptcha": 21 | return hcaptchaValidate(c) 22 | } 23 | 24 | return false, errors.New("invalid CaptchaType set") 25 | } 26 | 27 | func recaptchaValidate(c echo.Context) (bool, error) { 28 | // parse & validate request 29 | type Validation struct { 30 | Token string `validate:"required,min=1,max=1500" json:"g-recaptcha-response" form:"g-recaptcha-response"` 31 | } 32 | var validation Validation 33 | if _, err := Validate(c, &validation); err != nil { 34 | return false, err 35 | } 36 | 37 | return recaptcha.Confirm(c.RealIP(), validation.Token) 38 | } 39 | 40 | func hcaptchaValidate(c echo.Context) (bool, error) { 41 | // parse & validate request 42 | type Validation struct { 43 | Token string `validate:"required,min=1,max=1500" json:"h-captcha-response" form:"h-captcha-response"` 44 | } 45 | var validation Validation 46 | if _, err := Validate(c, &validation); err != nil { 47 | return false, err 48 | } 49 | 50 | return hcaptcha.Confirm(c.RealIP(), validation.Token) 51 | } 52 | -------------------------------------------------------------------------------- /helpers/DirSize.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func DirSize(path string) (int64, error) { 9 | var size int64 10 | err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { 11 | if err != nil { 12 | return err 13 | } 14 | if !info.IsDir() { 15 | size += info.Size() 16 | } 17 | return err 18 | }) 19 | return size, err 20 | } 21 | -------------------------------------------------------------------------------- /helpers/DynamicJwt.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/golang-jwt/jwt/v5" 7 | ) 8 | 9 | var jwtKey []byte 10 | 11 | func GenerateDynamicJWT[T jwt.Claims](claims *T, expire time.Duration, jwtKey []byte) (string, time.Time, error) { 12 | expirationTime := time.Now().Add(expire) 13 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, *claims) 14 | tokenString, err := token.SignedString(jwtKey) 15 | if err != nil { 16 | return "", time.Now(), err 17 | } 18 | return tokenString, expirationTime, nil 19 | } 20 | 21 | func VerifyDynamicJWT[T jwt.Claims](tknStr string, claims T, jwtKey []byte) (*jwt.Token, T, error) { 22 | tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 23 | return jwtKey, nil 24 | }) 25 | if err != nil { 26 | return nil, claims, err 27 | } 28 | return tkn, claims, nil 29 | } 30 | -------------------------------------------------------------------------------- /helpers/Folder.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | ) 7 | 8 | func FolderContainsFolder(folderId uint, searchingFolderId uint) (bool, error) { 9 | // list all child folders of the current folder 10 | var children []models.Folder 11 | if res := inits.DB.Where(&models.Folder{ 12 | ParentFolderID: folderId, 13 | }).Find(&children); res.Error != nil { 14 | return false, res.Error 15 | } 16 | 17 | // loop over the child folders 18 | for _, child := range children { 19 | // check if the current child folder matches the searching folder 20 | if child.ID == searchingFolderId { 21 | return true, nil 22 | } 23 | // check recursive if the child folders containing folders 24 | if contains, err := FolderContainsFolder(child.ID, searchingFolderId); err != nil || !contains { 25 | return contains, err 26 | } 27 | } 28 | 29 | // no search results 30 | return false, nil 31 | } 32 | -------------------------------------------------------------------------------- /helpers/GenM3u8Stream.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/models" 6 | "fmt" 7 | ) 8 | 9 | func GenM3u8Stream(dbLink *models.Link, qualitys *[]models.Quality, audio *models.Audio, JWT string) string { 10 | m3u8 := "#EXTM3U\n#EXT-X-VERSION:6" 11 | if audio != nil { 12 | m3u8 += fmt.Sprintf( 13 | "\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"AAC\",NAME=\"Subtitle\",LANGUAGE=\"%s\",URI=\"%s\"", 14 | audio.Lang, 15 | fmt.Sprintf("%s/%s/%s/audio/%s?jwt=%s", config.ENV.FolderVideoQualitysPub, dbLink.UUID, audio.UUID, audio.OutputFile, JWT), 16 | ) 17 | } 18 | for _, quality := range *qualitys { 19 | if quality.Type == "hls" && quality.Ready { 20 | m3u8 += fmt.Sprintf( 21 | "\n#EXT-X-STREAM-INF:BANDWIDTH=%d,AUDIO=\"AAC\",RESOLUTION=%s,CODECS=\"avc1.640015,mp4a.40.2\"\n%s", 22 | int64(quality.Height*quality.Width*2), 23 | fmt.Sprintf("%dx%d", quality.Width, quality.Height), 24 | fmt.Sprintf("%s/%s/%s/%s?jwt=%s", config.ENV.FolderVideoQualitysPub, dbLink.UUID, quality.Name, quality.OutputFile, JWT), 25 | ) 26 | } 27 | 28 | } 29 | return m3u8 30 | } 31 | -------------------------------------------------------------------------------- /helpers/GetUser.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | ) 7 | 8 | func GetUser(userId uint) (*models.User, error) { 9 | var user models.User 10 | 11 | if res := inits.DB.First(&user, userId); res.Error != nil { 12 | return nil, res.Error 13 | } 14 | 15 | return &user, nil 16 | } 17 | -------------------------------------------------------------------------------- /helpers/HashFile.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func HashFile(file string) (string, error) { 11 | f, err := os.Open(file) 12 | if err != nil { 13 | return "", err 14 | } 15 | defer f.Close() 16 | 17 | // obtain hash from file 18 | h := sha256.New() 19 | if _, err := io.Copy(h, f); err != nil { 20 | return "", err 21 | } 22 | return fmt.Sprintf("%x", h.Sum(nil)), nil 23 | } 24 | -------------------------------------------------------------------------------- /helpers/LimiterConfig.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/echo/v4/middleware" 9 | "golang.org/x/time/rate" 10 | ) 11 | 12 | func LimiterConfig(rate rate.Limit, burst int, expiration time.Duration) *middleware.RateLimiterConfig { 13 | return &middleware.RateLimiterConfig{ 14 | Skipper: LimiterWhitelistNext, 15 | Store: middleware.NewRateLimiterMemoryStoreWithConfig( 16 | middleware.RateLimiterMemoryStoreConfig{Rate: rate, Burst: burst, ExpiresIn: expiration}, 17 | ), 18 | IdentifierExtractor: func(c echo.Context) (string, error) { 19 | id := c.RealIP() 20 | return id, nil 21 | }, 22 | ErrorHandler: func(c echo.Context, err error) error { 23 | c.Logger().Error("Ratelimit Error", err) 24 | return c.NoContent(http.StatusInternalServerError) 25 | }, 26 | DenyHandler: func(c echo.Context, identifier string, err error) error { 27 | return c.String(http.StatusTooManyRequests, "Too fast") 28 | }, 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /helpers/LimiterWhitelistIps.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | var LimiterWhitelistIps = map[string]bool{ 10 | "127.0.0.1": true, 11 | } 12 | 13 | func LimiterWhitelistNext(c echo.Context) bool { 14 | // disable ratelimit by env 15 | if !*config.ENV.RatelimitEnabled { 16 | return true 17 | } 18 | // disable ratelimit by ip 19 | if LimiterWhitelistIps[c.RealIP()] { 20 | return true 21 | } 22 | 23 | // ratelimit enabled 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /helpers/Password.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func HashPassword(password string) (string, error) { 6 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 7 | return string(bytes), err 8 | } 9 | 10 | func CheckPasswordHash(password, hash string) bool { 11 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 12 | return err == nil 13 | } 14 | -------------------------------------------------------------------------------- /helpers/RemoveFromArray.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | func RemoveFromArray[T any](s []T, i int) []T { 4 | if len(s) == 0 || len(s) <= i || i < 0 { 5 | return s 6 | } 7 | s[i] = s[len(s)-1] 8 | return s[:len(s)-1] 9 | } 10 | -------------------------------------------------------------------------------- /helpers/UserRequestAsync.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | type UserRequestAsync struct { 11 | Count uint 12 | Map map[uint]bool 13 | Block bool 14 | } 15 | 16 | var UserRequestAsyncObj UserRequestAsync 17 | 18 | func (obj *UserRequestAsync) Blocked(userID uint) bool { 19 | if obj.Map[userID] || obj.Block { 20 | return true 21 | } 22 | 23 | return false 24 | } 25 | 26 | func (obj *UserRequestAsync) Sync(force bool) error { 27 | // query all users 28 | var userCount int64 29 | res := inits.DB.Model(&models.User{}). 30 | Select("id"). 31 | Count(&userCount) 32 | 33 | if res.Error != nil { 34 | return fmt.Errorf("failed to query all users to sync Request Map: %v", res.Error) 35 | } 36 | obj.Block = true 37 | if len(obj.Map) == 0 || force { 38 | obj.Map = make(map[uint]bool, userCount) 39 | obj.Block = false 40 | } else { 41 | // await until all requests are closed 42 | for obj.Block { 43 | time.Sleep(time.Millisecond * 200) 44 | if obj.Count == 0 { 45 | obj.Map = make(map[uint]bool, userCount) 46 | obj.Block = false 47 | } 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func (obj *UserRequestAsync) Start(userId uint) { 54 | obj.Map[userId] = true 55 | } 56 | 57 | func (obj *UserRequestAsync) End(userId uint) { 58 | obj.Map[userId] = false 59 | } 60 | -------------------------------------------------------------------------------- /helpers/Validator.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | var validate = validator.New() 13 | 14 | type ValidationError struct { 15 | FailedField string 16 | Tag string 17 | Value string 18 | } 19 | 20 | func ValidateStruct[T any](data T) []*ValidationError { 21 | var errors []*ValidationError 22 | err := validate.Struct(data) 23 | if err != nil { 24 | for _, err := range err.(validator.ValidationErrors) { 25 | var el ValidationError 26 | el.FailedField = err.Field() 27 | el.Tag = err.Tag() 28 | el.Value = err.Param() 29 | errors = append(errors, &el) 30 | } 31 | } 32 | return errors 33 | } 34 | 35 | func Validate[ValidationModel any](c echo.Context, validationModel *ValidationModel) (int, error) { 36 | err := c.Bind(validationModel) 37 | if err != nil { 38 | return http.StatusBadRequest, errors.New("malformated request") 39 | } 40 | 41 | if errors := ValidateStruct(validationModel); len(errors) > 0 { 42 | return http.StatusBadRequest, fmt.Errorf("%s [%s] : %s", errors[0].FailedField, errors[0].Tag, errors[0].Value) 43 | } 44 | 45 | return 0, nil 46 | } 47 | -------------------------------------------------------------------------------- /inits/Cache.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/patrickmn/go-cache" 7 | ) 8 | 9 | var Cache = cache.New(5*time.Minute, 10*time.Minute) 10 | -------------------------------------------------------------------------------- /inits/Captcha.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | 6 | "github.com/dpapathanasiou/go-recaptcha" 7 | "github.com/kirari04/go-hcaptcha" 8 | ) 9 | 10 | func Captcha() { 11 | recaptcha.Init(config.ENV.Captcha_Recaptcha_PrivateKey) 12 | hcaptcha.Init(config.ENV.Captcha_Hcaptcha_PrivateKey) 13 | } 14 | -------------------------------------------------------------------------------- /inits/Database.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "log" 5 | 6 | "gorm.io/driver/sqlite" 7 | "gorm.io/gorm" 8 | "gorm.io/gorm/logger" 9 | ) 10 | 11 | var DB *gorm.DB 12 | 13 | func Database() { 14 | newDb, err := gorm.Open(sqlite.Open("./database/database.sqlite"), &gorm.Config{ 15 | Logger: logger.Default.LogMode(logger.Silent), 16 | DisableForeignKeyConstraintWhenMigrating: true, 17 | IgnoreRelationshipsWhenMigrating: true, 18 | }) 19 | if err != nil { 20 | log.Panicf("Failed to connect database: %s", err.Error()) 21 | } 22 | 23 | DB = newDb 24 | } 25 | -------------------------------------------------------------------------------- /inits/Folders.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func Folders() { 10 | // create folders 11 | createFolders := []string{"./database", config.ENV.FolderVideoQualitysPriv, config.ENV.FolderVideoUploadsPriv, "./logs"} 12 | for _, createFolder := range createFolders { 13 | if fileInfo, err := os.Stat(createFolder); err != nil || !fileInfo.IsDir() { 14 | if err := os.MkdirAll(createFolder, 0766); err != nil { 15 | log.Panicf("Failed to generate essential folder: %s", createFolder) 16 | } 17 | log.Printf("Generated folder: %s\n", createFolder) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /inits/Models.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "ch/kirari04/videocms/models" 5 | "log" 6 | ) 7 | 8 | func Models() { 9 | if DB == nil { 10 | log.Fatalln("DB is nil while attempting to migrate") 11 | } 12 | mustRun(DB.AutoMigrate(&models.User{})) 13 | mustRun(DB.AutoMigrate(&models.Folder{})) 14 | mustRun(DB.AutoMigrate(&models.File{})) 15 | mustRun(DB.AutoMigrate(&models.Link{})) 16 | mustRun(DB.AutoMigrate(&models.Quality{})) 17 | mustRun(DB.AutoMigrate(&models.Subtitle{})) 18 | mustRun(DB.AutoMigrate(&models.Audio{})) 19 | mustRun(DB.AutoMigrate(&models.UploadSession{})) 20 | mustRun(DB.AutoMigrate(&models.UploadChunck{})) 21 | mustRun(DB.AutoMigrate(&models.Webhook{})) 22 | mustRun(DB.AutoMigrate(&models.WebPage{})) 23 | mustRun(DB.AutoMigrate(&models.Setting{})) 24 | mustRun(DB.AutoMigrate(&models.SystemResource{})) 25 | mustRun(DB.AutoMigrate(&models.Tag{})) 26 | mustRun(DB.AutoMigrate(&models.TagLinks{})) 27 | } 28 | 29 | func mustRun(err error) { 30 | if err != nil { 31 | log.Fatalln("Failed to migrate: ", err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /inits/Server.go: -------------------------------------------------------------------------------- 1 | package inits 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "context" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "runtime" 14 | "strings" 15 | "time" 16 | 17 | "github.com/labstack/echo/v4" 18 | "github.com/labstack/echo/v4/middleware" 19 | "golang.org/x/net/http2" 20 | ) 21 | 22 | var App *echo.Echo 23 | var Api echo.Group 24 | 25 | type Template struct { 26 | templates *template.Template 27 | } 28 | 29 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 30 | return t.templates.ExecuteTemplate(w, name, data) 31 | } 32 | 33 | func Server() { 34 | htmlTemplate := &Template{ 35 | templates: template.Must(template.ParseGlob("views/*.html")), 36 | } 37 | trustedProxies := []string{} 38 | if *config.ENV.CloudflareEnabled { 39 | trustedProxies = append(trustedProxies, []string{ 40 | "173.245.48.0/20", 41 | "103.21.244.0/22", 42 | "103.22.200.0/22", 43 | "103.31.4.0/22", 44 | "141.101.64.0/18", 45 | "108.162.192.0/18", 46 | "190.93.240.0/20", 47 | "188.114.96.0/20", 48 | "197.234.240.0/22", 49 | "198.41.128.0/17", 50 | "162.158.0.0/15", 51 | "104.16.0.0/13", 52 | "104.24.0.0/14", 53 | "172.64.0.0/13", 54 | "131.0.72.0/22", 55 | "2400:cb00::/32", 56 | "2606:4700::/32", 57 | "2803:f800::/32", 58 | "2405:b500::/32", 59 | "2405:8100::/32", 60 | "2a06:98c0::/29", 61 | "2c0f:f248::/32", 62 | }...) 63 | } 64 | app := echo.New() 65 | app.Renderer = htmlTemplate 66 | trustOptions := []echo.TrustOption{ 67 | echo.TrustLoopback(false), // e.g. ipv4 start with 127. 68 | echo.TrustLinkLocal(false), // e.g. ipv4 start with 169.254 69 | echo.TrustPrivateNet(false), // e.g. ipv4 start with 10. or 192.168 70 | } 71 | for _, trustedIpRanges := range trustedProxies { 72 | _, ipNet, err := net.ParseCIDR(trustedIpRanges) 73 | if err != nil { 74 | app.Logger.Error("Failed to parse ip range", err) 75 | continue 76 | } 77 | trustOptions = append(trustOptions, echo.TrustIPRange(ipNet)) 78 | } 79 | 80 | app.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...) 81 | app.HTTPErrorHandler = func(errors error, c echo.Context) { 82 | code := http.StatusInternalServerError 83 | if he, ok := errors.(*echo.HTTPError); ok { 84 | code = he.Code 85 | } 86 | 87 | if code == 404 { 88 | if err := c.Render(code, "404.html", echo.Map{}); err != nil { 89 | c.Logger().Error(err) 90 | } 91 | } else { 92 | c.Logger().Error(errors) 93 | c.NoContent(code) 94 | } 95 | } 96 | 97 | // recovering from panics 98 | app.Use(middleware.Recover()) 99 | 100 | // body limit 101 | e := echo.New() 102 | postMaxSize := int64(float64(config.ENV.MaxPostSize) / 1000) 103 | e.Use(middleware.BodyLimit(fmt.Sprintf("%dk", postMaxSize))) 104 | 105 | // Compression middleware 106 | app.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 107 | Skipper: func(c echo.Context) bool { 108 | res := strings.HasPrefix(c.Path(), config.ENV.FolderVideoQualitysPub) 109 | if res { 110 | c.Response().Header().Add("Compress", "LevelDisabled") 111 | } else { 112 | c.Response().Header().Add("Compress", "LevelBestCompression") 113 | } 114 | return res 115 | }, 116 | })) 117 | 118 | // cors configuration 119 | app.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 120 | AllowOrigins: []string{config.ENV.CorsAllowOrigins}, 121 | AllowHeaders: []string{config.ENV.CorsAllowHeaders}, 122 | AllowCredentials: *config.ENV.CorsAllowCredentials, 123 | MaxAge: 7200, 124 | })) 125 | 126 | // Logging 127 | app.Use(middleware.Logger()) 128 | 129 | App = app 130 | Api = *app.Group("/api") 131 | } 132 | 133 | func ServerStart() { 134 | // Start server 135 | go func() { 136 | if err := App.StartH2CServer(config.ENV.Host, &http2.Server{ 137 | MaxConcurrentStreams: uint32(runtime.NumGoroutine()), 138 | MaxReadFrameSize: 1048576, 139 | IdleTimeout: 10 * time.Second, 140 | }); err != nil && err != http.ErrServerClosed { 141 | App.Logger.Fatal("shutting down the server") 142 | } 143 | }() 144 | 145 | // Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds. 146 | // Use a buffered channel to avoid missing signals as recommended for signal.Notify 147 | quit := make(chan os.Signal, 1) 148 | signal.Notify(quit, os.Interrupt) 149 | <-quit 150 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 151 | defer cancel() 152 | if err := App.Shutdown(ctx); err != nil { 153 | App.Logger.Fatal(err) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /logic/CreateFolder.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func CreateFolder(folderName string, toFolder uint, userId uint) (status int, newFolder *models.Folder, err error) { 14 | //check if requested folder exists (if set) 15 | if toFolder > 0 { 16 | res := inits.DB.First(&models.Folder{}, toFolder) 17 | if res.Error != nil { 18 | return http.StatusBadRequest, nil, errors.New("parent folder doesn't exist") 19 | } 20 | } 21 | 22 | // create folder 23 | folder := models.Folder{ 24 | Name: folderName, 25 | ParentFolderID: toFolder, 26 | UserID: userId, 27 | } 28 | 29 | if res := inits.DB.Model(&models.Folder{}).Create(&folder); res.Error != nil { 30 | log.Printf("Error creating new folder: %v", res.Error) 31 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 32 | } 33 | 34 | return http.StatusOK, &folder, nil 35 | } 36 | -------------------------------------------------------------------------------- /logic/CreateTag.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func CreateTag(tagName string, toLinkId uint, userId uint) (status int, newTag *models.Tag, err error) { 14 | //check if requested folder exists (if set) 15 | var link models.Link 16 | if err := inits.DB.First(&link, toLinkId).Error; err != nil { 17 | return http.StatusBadRequest, nil, errors.New("link doesn't exist") 18 | } 19 | if link.UserID != userId { 20 | return http.StatusBadRequest, nil, errors.New("link doesn't exist") 21 | } 22 | 23 | // check if tag already exists else create new 24 | var tag models.Tag 25 | if err := inits.DB.Where(&models.Tag{Name: tagName, UserId: userId}).First(&tag).Error; err != nil { 26 | tag = models.Tag{Name: tagName, UserId: userId} 27 | if err := inits.DB.Create(&tag).Error; err != nil { 28 | return http.StatusBadRequest, nil, errors.New("failed to create new tag") 29 | } 30 | } 31 | 32 | if err := inits.DB.Model(&link).Association("Tags").Append(&tag); err != nil { 33 | log.Printf("Error adding new tag: %v", err) 34 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 35 | } 36 | 37 | return http.StatusOK, &tag, nil 38 | } 39 | -------------------------------------------------------------------------------- /logic/CreateUploadChunck.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "math" 12 | "net/http" 13 | "os" 14 | 15 | "github.com/labstack/echo/v4" 16 | ) 17 | 18 | func CreateUploadChunck(index uint, sessionToken string, fromFile string) (status int, response string, err error) { 19 | // validate token 20 | token, claims, err := helpers.VerifyDynamicJWT(sessionToken, &models.UploadSessionClaims{}, []byte(config.ENV.JwtUploadSecretKey)) 21 | if err != nil || claims == nil { 22 | log.Printf("err: %v", err) 23 | return http.StatusBadRequest, "", errors.New("broken upload session token") 24 | } 25 | if !token.Valid { 26 | return http.StatusBadRequest, "", errors.New("invalid upload session token") 27 | } 28 | 29 | //check if session still active 30 | uploadSession := models.UploadSession{} 31 | if res := inits.DB. 32 | Where(&models.UploadSession{ 33 | UUID: (*claims).UUID, 34 | }).First(&uploadSession); res.Error != nil { 35 | return http.StatusNotFound, "", errors.New("upload session not found") 36 | } 37 | 38 | // check chunck size 39 | chunckFile, err := os.Open(fromFile) 40 | if err != nil { 41 | log.Printf("Failed to open uploaded chunck: %v", err) 42 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 43 | } 44 | chunckFileStat, err := chunckFile.Stat() 45 | if err != nil { 46 | log.Printf("Failed to read stat from uploaded chunck: %v", err) 47 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 48 | } 49 | maxchunckFileSize := int64(math.Ceil(float64(uploadSession.Size) / float64(uploadSession.ChunckCount))) 50 | if chunckFileStat.Size() > maxchunckFileSize+100 { 51 | return http.StatusRequestEntityTooLarge, "", echo.ErrStatusRequestEntityTooLarge 52 | } 53 | 54 | // check chunck count 55 | if int(index) >= uploadSession.ChunckCount { 56 | return http.StatusBadRequest, "", fmt.Errorf("chunck index is too high: chunck index: %d vs max index: %d", index, uploadSession.ChunckCount) 57 | } 58 | 59 | /* 60 | Because of parallelism we don't check if the index has already been uploaded. 61 | Incase it already has been uploaded the new one will just overwrite the old one. 62 | */ 63 | chunckPath := fmt.Sprintf("%s/%v.chunck", uploadSession.SessionFolder, index) 64 | if err := os.Rename(fromFile, chunckPath); err != nil { 65 | log.Printf("Failed to move uploaded chunck into upload session folder: %v", err) 66 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 67 | } 68 | 69 | existingUploadedChunck := models.UploadChunck{} 70 | if res := inits.DB.Where(&models.UploadChunck{ 71 | Index: index, 72 | Path: chunckPath, 73 | UploadSessionID: uploadSession.ID, 74 | }).FirstOrCreate(&existingUploadedChunck); res.Error != nil { 75 | log.Printf("Failed to add uploaded chunck into db: %v", res.Error) 76 | log.Printf("Removing Chunck: %v", os.Remove(chunckPath)) 77 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 78 | } 79 | 80 | return http.StatusOK, "ok", nil 81 | } 82 | -------------------------------------------------------------------------------- /logic/CreateUploadFile.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/http" 13 | "os" 14 | 15 | "github.com/google/uuid" 16 | "github.com/labstack/echo/v4" 17 | ) 18 | 19 | /* 20 | */ 21 | func CreateUploadFile(sessionToken string, userId uint) (status int, response *models.Link, err error) { 22 | // validate token 23 | token, claims, err := helpers.VerifyDynamicJWT(sessionToken, &models.UploadSessionClaims{}, []byte(config.ENV.JwtUploadSecretKey)) 24 | if err != nil && claims != nil { 25 | log.Printf("err: %v", err) 26 | return http.StatusBadRequest, nil, errors.New("broken upload session token") 27 | } 28 | if !token.Valid { 29 | return http.StatusBadRequest, nil, errors.New("invalid upload session token") 30 | } 31 | if (*claims).UserID != userId { 32 | return http.StatusForbidden, nil, echo.ErrForbidden 33 | } 34 | 35 | //check if session still active 36 | uploadSession := models.UploadSession{} 37 | if res := inits.DB. 38 | Where(&models.UploadSession{ 39 | UUID: (*claims).UUID, 40 | UserID: userId, 41 | }).First(&uploadSession); res.Error != nil { 42 | return http.StatusNotFound, nil, errors.New("upload session not found") 43 | } 44 | 45 | //list all chuncks 46 | uploadChuncks := []models.UploadChunck{} 47 | if res := inits.DB. 48 | Where(&models.UploadChunck{ 49 | UploadSessionID: uploadSession.ID, 50 | }). 51 | Order("`index` ASC"). 52 | Find(&uploadChuncks); res.Error != nil { 53 | log.Printf("Failed to create find upload chuncks: %v", res.Error) 54 | return http.StatusNotFound, nil, errors.New("upload chuncks not found") 55 | } 56 | if len(uploadChuncks) != uploadSession.ChunckCount { 57 | return http.StatusBadRequest, nil, fmt.Errorf("missing Chuncks: uploaded %v, required %v", len(uploadChuncks), uploadSession.ChunckCount) 58 | } 59 | 60 | // delete any missing files or sessions inside database if anything failes or it successfully finishes 61 | defer createUploadFileCleanup(&uploadSession) 62 | 63 | // open finalFile (copy destination of the chuncks) 64 | finalFilePath := fmt.Sprintf("%v/example.mkv", uploadSession.SessionFolder) 65 | finalFile, err := os.OpenFile(finalFilePath, os.O_CREATE|os.O_WRONLY, 0766) 66 | if err != nil { 67 | log.Printf("Failed to create final file: %v", err) 68 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 69 | } 70 | 71 | // copy uploaded chuncks into final file 72 | var written int64 73 | for _, uploadChunck := range uploadChuncks { 74 | openedChunck, err := os.Open(uploadChunck.Path) 75 | if err != nil { 76 | log.Printf("Failed to read chunck %v: %v", uploadChunck.Path, err) 77 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 78 | } 79 | n, err := io.Copy(finalFile, openedChunck) 80 | if err != nil { 81 | log.Printf("Failed to copy chunck %v: %v", uploadChunck.Path, err) 82 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 83 | } 84 | written += n 85 | if err := openedChunck.Close(); err != nil { 86 | log.Printf("Failed to close chunck %v: %v", uploadChunck.Path, err) 87 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 88 | } 89 | } 90 | 91 | if err := finalFile.Close(); err != nil { 92 | log.Printf("Failed to close final file: %v", err) 93 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 94 | } 95 | 96 | // check file size 97 | finalFilePathInfo, err := os.Stat(finalFilePath) 98 | if err != nil { 99 | log.Printf("Failed to read filestat of final file: %v", err) 100 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 101 | } 102 | if finalFilePathInfo.Size() != uploadSession.Size { 103 | return http.StatusConflict, nil, fmt.Errorf("the uploaded file size doesnt match with the uploaded file: server %v, client %v", finalFilePathInfo.Size(), uploadSession.Size) 104 | } 105 | 106 | // create file 107 | fileId := uuid.NewString() 108 | filePath := fmt.Sprintf("%s/%s.%s", config.ENV.FolderVideoUploadsPriv, fileId, "tmp") 109 | if err := os.Rename(finalFilePath, filePath); err != nil { 110 | log.Printf("Failed to copy final file to destination: %v", err) 111 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 112 | } 113 | status, dbLink, cloned, err := CreateFile(&filePath, uploadSession.ParentFolderID, uploadSession.Name, fileId, uploadSession.Size, userId) 114 | if err != nil { 115 | os.Remove(filePath) 116 | return status, nil, err 117 | } 118 | if cloned { 119 | os.Remove(filePath) 120 | } 121 | 122 | return status, dbLink, nil 123 | } 124 | 125 | func createUploadFileCleanup(uploadSession *models.UploadSession) { 126 | if err := os.RemoveAll(uploadSession.SessionFolder); err != nil { 127 | log.Printf("[WARNING] createUploadFileCleanup -> remove session folder: %v\n", err) 128 | } 129 | if res := inits.DB. 130 | Model(&models.UploadChunck{}). 131 | Where(&models.UploadChunck{ 132 | UploadSessionID: uploadSession.ID, 133 | }). 134 | Delete(&models.UploadChunck{}); res.Error != nil { 135 | log.Printf("[WARNING] createUploadFileCleanup -> remove upload chuncks from database (%d): %v\n", uploadSession.ID, res.Error) 136 | } 137 | if res := inits.DB. 138 | Delete(&models.UploadSession{}, uploadSession.ID); res.Error != nil { 139 | log.Printf("[WARNING] createUploadFileCleanup -> remove upload session from database (%d): %v\n", uploadSession.ID, res.Error) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /logic/CreateUploadSession.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "math" 12 | "net/http" 13 | "os" 14 | "time" 15 | 16 | "github.com/labstack/echo/v4" 17 | ) 18 | 19 | type CreateUploadSessionResponse struct { 20 | Token string 21 | UUID string 22 | ChunckCount int 23 | Expires time.Time 24 | } 25 | 26 | /* 27 | This functions shouldn't be run concurrent 28 | else the user would be able to spam the endpoint and 29 | use the split delay in the db lookup to have more concurrent upload sessions then defined 30 | */ 31 | func CreateUploadSession(toFolder uint, fileName string, uploadSessionUUID string, fileSize int64, userId uint) (status int, response *CreateUploadSessionResponse, err error) { 32 | 33 | if helpers.UserRequestAsyncObj.Blocked(userId) { 34 | return http.StatusTooManyRequests, nil, errors.New("wait until the previous delete request finished") 35 | } 36 | helpers.UserRequestAsyncObj.Start(userId) 37 | defer helpers.UserRequestAsyncObj.End(userId) 38 | 39 | //check if requested folder exists (if set) 40 | if toFolder > 0 { 41 | res := inits.DB.First(&models.Folder{}, toFolder) 42 | if res.Error != nil { 43 | return http.StatusBadRequest, nil, errors.New("parent folder doesn't exist") 44 | } 45 | } 46 | 47 | //check requested filesize size 48 | if fileSize > config.ENV.MaxUploadFilesize { 49 | return http.StatusRequestEntityTooLarge, nil, fmt.Errorf("exceeded max upload filesize: %v", config.ENV.MaxUploadFilesize) 50 | } 51 | 52 | // get user settings 53 | User, err := helpers.GetUser(userId) 54 | if err != nil { 55 | log.Printf("Failed to fetch user %v: %v", userId, err) 56 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 57 | } 58 | 59 | //check for active upload sessions 60 | var activeUploadSessions int64 61 | if res := inits.DB. 62 | Model(&models.UploadSession{}). 63 | Where(&models.UploadSession{ 64 | UserID: userId, 65 | }).Count(&activeUploadSessions); res.Error != nil { 66 | log.Printf("Failed to calc activeUploadSessions: %v : %v", activeUploadSessions, res.Error) 67 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 68 | } 69 | if activeUploadSessions >= config.ENV.MaxUploadSessions { 70 | if activeUploadSessions >= User.Settings.UploadSessionsMax { 71 | return http.StatusBadRequest, nil, fmt.Errorf("exceeded max upload sessions: %v", config.ENV.MaxUploadSessions) 72 | } 73 | } 74 | 75 | // create upload session folder 76 | sessionFolder := fmt.Sprintf("%s/%s", config.ENV.FolderVideoUploadsPriv, uploadSessionUUID) 77 | if err := os.MkdirAll(sessionFolder, 0766); err != nil { 78 | log.Printf("Failed to create upload session folder: %v : %v", sessionFolder, err) 79 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 80 | } 81 | 82 | // create session 83 | chunckCount := math.Ceil(float64(fileSize) / float64(config.ENV.MaxUploadChuncksize)) 84 | newSession := models.UploadSession{ 85 | Name: fileName, 86 | UUID: uploadSessionUUID, 87 | Size: fileSize, 88 | ChunckCount: int(chunckCount), 89 | SessionFolder: sessionFolder, 90 | ParentFolderID: toFolder, 91 | UserID: userId, 92 | } 93 | if res := inits.DB.Create(&newSession); res.Error != nil { 94 | log.Printf("Failed to create new upload session: %v", res.Error) 95 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 96 | } 97 | 98 | claims := models.UploadSessionClaims{ 99 | UUID: uploadSessionUUID, 100 | UserID: userId, 101 | } 102 | 103 | maxUploadDuration := time.Hour * 2 104 | token, expirationTime, err := helpers.GenerateDynamicJWT[models.UploadSessionClaims](&claims, maxUploadDuration, []byte(config.ENV.JwtUploadSecretKey)) 105 | if err != nil { 106 | log.Printf("Failed to generate jwt token for upload session: %v", err) 107 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 108 | } 109 | 110 | return http.StatusOK, &CreateUploadSessionResponse{ 111 | Token: token, 112 | Expires: expirationTime, 113 | UUID: uploadSessionUUID, 114 | ChunckCount: int(chunckCount), 115 | }, nil 116 | } 117 | -------------------------------------------------------------------------------- /logic/CreateWebhook.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | func CreateWebhook(webhookValidation *models.WebhookCreateValidation, userID uint) (status int, response string, err error) { 13 | if res := inits.DB.Create(&models.Webhook{ 14 | Name: webhookValidation.Name, 15 | Url: webhookValidation.Url, 16 | Rpm: webhookValidation.Rpm, 17 | ReqQuery: webhookValidation.ReqQuery, 18 | ResField: webhookValidation.ResField, 19 | UserID: userID, 20 | }); res.Error != nil { 21 | log.Printf("Failed to create webhook: %v", res.Error) 22 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 23 | } 24 | return http.StatusOK, "ok", nil 25 | } 26 | -------------------------------------------------------------------------------- /logic/DeleteFiles.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func DeleteFiles(fileValidation *models.LinksDeleteValidation, userID uint) (status int, err error) { 16 | if len(fileValidation.LinkIDs) == 0 { 17 | return http.StatusBadRequest, errors.New("array LinkIDs is empty") 18 | } 19 | if int64(len(fileValidation.LinkIDs)) > config.ENV.MaxItemsMultiDelete { 20 | return http.StatusBadRequest, errors.New("max requested items exceeded") 21 | } 22 | 23 | //check if requested files exists 24 | linkIdDeleteMap := make(map[uint]bool, len(fileValidation.LinkIDs)) 25 | linkIdDeleteList := []uint{} 26 | for _, LinkValidation := range fileValidation.LinkIDs { 27 | if res := inits.DB.First(&models.Link{ 28 | UserID: userID, 29 | }, LinkValidation.LinkID); res.Error != nil { 30 | return http.StatusBadRequest, fmt.Errorf("linkID (%d) doesn't exist", LinkValidation.LinkID) 31 | } 32 | if linkIdDeleteMap[LinkValidation.LinkID] { 33 | return http.StatusBadRequest, fmt.Errorf("the files have to be distinct. File %d is dublicate", LinkValidation.LinkID) 34 | } 35 | linkIdDeleteList = append(linkIdDeleteList, LinkValidation.LinkID) 36 | linkIdDeleteMap[LinkValidation.LinkID] = true 37 | } 38 | 39 | // delete links 40 | if res := inits.DB.Delete(&models.Link{}, linkIdDeleteList); res.Error != nil { 41 | log.Printf("Failed to delete links: %v", res.Error) 42 | return http.StatusInternalServerError, echo.ErrInternalServerError 43 | } 44 | 45 | return http.StatusOK, nil 46 | } 47 | -------------------------------------------------------------------------------- /logic/DeleteFolders.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/models" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | /* 14 | This function shouldnt be runned in concurrency. 15 | The reason for that is, if the user woulf start the process of delete for the root folder 16 | and shortly after calls the delete method for the root folder too, it would try to list the 17 | whole folder tree and delete all folders & files again. To prevent that there is an map variable 18 | that should prevent the user from calling this method multiple times concurrently. 19 | */ 20 | func DeleteFolders(folderValidation *models.FoldersDeleteValidation, userID uint) (status int, err error) { 21 | 22 | if helpers.UserRequestAsyncObj.Blocked(userID) { 23 | return http.StatusTooManyRequests, errors.New("wait until the previous delete request finished") 24 | } 25 | helpers.UserRequestAsyncObj.Start(userID) 26 | defer helpers.UserRequestAsyncObj.End(userID) 27 | 28 | if len(folderValidation.FolderIDs) == 0 { 29 | return http.StatusBadRequest, errors.New("array FolderIDs is empty") 30 | } 31 | 32 | if int64(len(folderValidation.FolderIDs)) > config.ENV.MaxItemsMultiDelete { 33 | return http.StatusBadRequest, errors.New("max requested items exceeded") 34 | } 35 | 36 | //check if requested folders exists 37 | reqFolderIdDeleteMap := make(map[uint]bool, len(folderValidation.FolderIDs)) 38 | reqFolderIdDeleteList := []uint{} 39 | var parentFolderID uint = 0 40 | for i, FolderValidation := range folderValidation.FolderIDs { 41 | var dbFolder = models.Folder{ 42 | UserID: userID, 43 | } 44 | if res := inits.DB.First(&dbFolder, FolderValidation.FolderID); res.Error != nil { 45 | return http.StatusBadRequest, fmt.Errorf("FolderID (%d) doesn't exist", FolderValidation.FolderID) 46 | } 47 | // check if has same parent folder 48 | if i == 0 { 49 | parentFolderID = dbFolder.ParentFolderID 50 | } 51 | if i > 0 { 52 | if parentFolderID != dbFolder.ParentFolderID { 53 | return http.StatusBadRequest, fmt.Errorf( 54 | "all folders have to share the same parent folder. Folder %d doesnt: %d (required) vs %d (actual)", 55 | FolderValidation.FolderID, 56 | parentFolderID, 57 | dbFolder.ParentFolderID, 58 | ) 59 | } 60 | } 61 | 62 | if reqFolderIdDeleteMap[FolderValidation.FolderID] { 63 | return http.StatusBadRequest, fmt.Errorf( 64 | "the folders have to be distinct. Folder %d is dublicate", 65 | FolderValidation.FolderID, 66 | ) 67 | } 68 | // add folder to todo list 69 | reqFolderIdDeleteList = append(reqFolderIdDeleteList, FolderValidation.FolderID) 70 | reqFolderIdDeleteMap[FolderValidation.FolderID] = true 71 | } 72 | 73 | // query all containing 74 | folders := []uint{} 75 | files := []models.LinkDeleteValidation{} 76 | 77 | for _, reqFolderId := range reqFolderIdDeleteList { 78 | listFolders(reqFolderId, &folders, &files) 79 | folders = append(folders, reqFolderId) 80 | listFiles(reqFolderId, &files) 81 | } 82 | 83 | if len(files) > 0 { 84 | if status, err := DeleteFiles(&models.LinksDeleteValidation{ 85 | LinkIDs: files, 86 | }, userID); err != nil { 87 | return status, fmt.Errorf("failed to delete all files from folders: %v", err) 88 | } 89 | } 90 | 91 | if res := inits.DB.Delete(&models.Folder{}, folders); res.Error != nil { 92 | return status, fmt.Errorf("failed to delete all folders: %v", err) 93 | } 94 | 95 | return http.StatusOK, nil 96 | } 97 | 98 | func listFolders(folderId uint, folders *[]uint, files *[]models.LinkDeleteValidation) { 99 | var folderList []models.Folder 100 | inits.DB.Select("id"). 101 | Where(&models.Folder{ 102 | ParentFolderID: folderId, 103 | }). 104 | Find(&folderList) 105 | for _, id := range folderList { 106 | listFolders(id.ID, folders, files) 107 | *folders = append(*folders, id.ID) 108 | listFiles(id.ID, files) 109 | } 110 | } 111 | 112 | func listFiles(folderId uint, files *[]models.LinkDeleteValidation) { 113 | var fileList []models.Link 114 | inits.DB.Select("id"). 115 | Where(&models.Link{ 116 | ParentFolderID: folderId, 117 | }). 118 | Find(&fileList) 119 | for _, id := range fileList { 120 | *files = append(*files, models.LinkDeleteValidation{ 121 | LinkID: id.ID, 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /logic/DeleteTag.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func DeleteTag(tagId uint, fromLinkId uint, userId uint) (status int, err error) { 14 | //check if requested folder exists (if set) 15 | var link models.Link 16 | if err := inits.DB.First(&link, fromLinkId).Error; err != nil { 17 | return http.StatusBadRequest, errors.New("link doesn't exist") 18 | } 19 | if link.UserID != userId { 20 | return http.StatusBadRequest, errors.New("link doesn't exist") 21 | } 22 | 23 | // check if tag exists 24 | var tag models.Tag 25 | if err := inits.DB.First(&tag, tagId).Error; err != nil { 26 | return http.StatusBadRequest, errors.New("tag doesn't exist") 27 | } 28 | 29 | if tag.UserId != userId { 30 | return http.StatusBadRequest, errors.New("tag doesn't exist") 31 | } 32 | 33 | if err := inits.DB.Model(&link).Association("Tags").Delete(&tag); err != nil { 34 | log.Printf("Error removing tag: %v", err) 35 | return http.StatusInternalServerError, echo.ErrInternalServerError 36 | } 37 | 38 | return http.StatusOK, nil 39 | } 40 | -------------------------------------------------------------------------------- /logic/DeleteWebhook.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func DeleteWebhook(validated *models.WebhookDeleteValidation, userID uint) (status int, response string, err error) { 15 | 16 | var webhook models.Webhook 17 | if res := inits.DB.Where(&models.Webhook{ 18 | UserID: userID, 19 | }).First(&webhook, validated.WebhookID); res.Error != nil { 20 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 21 | return http.StatusNotFound, "", echo.ErrNotFound 22 | } 23 | log.Printf("Failed to query webhook %v: %v", validated.WebhookID, res.Error) 24 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 25 | } 26 | if res := inits.DB.Delete(&webhook); res.Error != nil { 27 | log.Printf("Failed to delete webhook %v: %v", validated.WebhookID, res.Error) 28 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 29 | } 30 | 31 | return http.StatusOK, "ok", nil 32 | } 33 | -------------------------------------------------------------------------------- /logic/GetAccount.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/labstack/echo/v4" 14 | "golang.org/x/sys/unix" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | type GetAccountResponse struct { 19 | Username string 20 | Admin bool 21 | Email string 22 | Balance float64 23 | Storage int64 24 | Used int64 25 | Files int64 26 | Settings models.UserSettings 27 | } 28 | 29 | func GetAccount(userID uint) (status int, response *GetAccountResponse, err error) { 30 | if data, found := inits.Cache.Get(fmt.Sprintf("account-%d", userID)); found { 31 | res := data.(GetAccountResponse) 32 | return http.StatusOK, &res, nil 33 | } 34 | 35 | var dbUser models.User 36 | if res := inits.DB.Find(&dbUser, userID); res.Error != nil { 37 | log.Printf("Failed to query user: %v", res.Error) 38 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 39 | } 40 | type DBResponse struct { 41 | UploadedFiles int64 42 | StorageUsed int64 43 | } 44 | var dbUsed DBResponse 45 | if res := inits.DB.Model(&models.Link{}). 46 | Joins("inner join files on files.id = links.file_id"). 47 | Select("COUNT(links.id) AS uploaded_files", "SUM(files.size) AS storage_used"). 48 | Where(&models.Link{ 49 | UserID: userID, 50 | }). 51 | Group("links.user_id"). 52 | First(&dbUsed); res.Error != nil { 53 | if !errors.Is(res.Error, gorm.ErrRecordNotFound) { 54 | log.Printf("Failed to query UploadedFiles & StorageUsed: %v", res.Error) 55 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 56 | } 57 | } 58 | 59 | if dbUser.Storage == 0 { 60 | // if user has no specific limit we show the physical available space 61 | 62 | folderPath := config.ENV.FolderVideoUploadsPriv 63 | var stat unix.Statfs_t // Or unix.Statfs_t 64 | if err := unix.Statfs(folderPath, &stat); err == nil { 65 | availableBytes := stat.Bavail * uint64(stat.Bsize) 66 | 67 | dbUser.Storage = int64(availableBytes) 68 | } else { 69 | dbUser.Storage = -1 70 | } 71 | } 72 | 73 | newResponse := GetAccountResponse{ 74 | Username: dbUser.Username, 75 | Admin: dbUser.Admin, 76 | Email: dbUser.Email, 77 | Balance: dbUser.Balance, 78 | Storage: dbUser.Storage, 79 | Settings: dbUser.Settings, 80 | Used: dbUsed.StorageUsed, 81 | Files: dbUsed.UploadedFiles, 82 | } 83 | // save in cache 84 | inits.Cache.Set(fmt.Sprintf("account-%d", userID), newResponse, time.Minute) 85 | 86 | return http.StatusOK, &newResponse, nil 87 | } 88 | -------------------------------------------------------------------------------- /logic/GetAudioData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | func GetAudioData(requestValidation *models.AudioGetValidation) (status int, filePath *string, err error) { 14 | reFILE := regexp.MustCompile(`^audio[0-9]{0,4}\.(m3u8|ts|wav|mp3|ogg)$`) 15 | 16 | if !reFILE.MatchString(requestValidation.FILE) { 17 | return http.StatusBadRequest, nil, errors.New("bad file format") 18 | } 19 | 20 | //translate link id to file id 21 | var dbLink models.Link 22 | 23 | if dbRes := inits.DB. 24 | Model(&models.Link{}). 25 | Preload("File"). 26 | Preload("File.Audios"). 27 | Where(&models.Link{ 28 | UUID: requestValidation.UUID, 29 | }). 30 | First(&dbLink); dbRes.Error != nil { 31 | return http.StatusNotFound, nil, errors.New("audio doesn't exist") 32 | } 33 | 34 | //check if audio uuid exists 35 | audioExists := false 36 | for _, audio := range dbLink.File.Audios { 37 | if audio.Ready && 38 | audio.UUID == requestValidation.AUDIOUUID { 39 | audioExists = true 40 | } 41 | } 42 | if !audioExists { 43 | return http.StatusNotFound, nil, errors.New("audio doesn't exist") 44 | } 45 | 46 | resPath := fmt.Sprintf("%s/%s/%s/%s", config.ENV.FolderVideoQualitysPriv, dbLink.File.UUID, requestValidation.AUDIOUUID, requestValidation.FILE) 47 | return http.StatusOK, &resPath, nil 48 | } 49 | -------------------------------------------------------------------------------- /logic/GetFile.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | type GetFileRespQuali struct { 15 | Name string 16 | Type string 17 | Height int64 18 | Width int64 19 | AvgFrameRate float64 20 | Ready bool 21 | Failed bool 22 | Progress float64 23 | Size int64 24 | } 25 | type GetFileRespSub struct { 26 | Name string 27 | Type string 28 | Lang string 29 | Ready bool 30 | } 31 | type GetFileRespAudio struct { 32 | Name string 33 | Type string 34 | Lang string 35 | Ready bool 36 | } 37 | type GetFileRespTag struct { 38 | ID uint 39 | Name string 40 | } 41 | type GetFileResp struct { 42 | CreatedAt time.Time 43 | UpdatedAt time.Time 44 | ID uint 45 | UUID string 46 | Name string 47 | Thumbnail string 48 | ParentFolderID uint 49 | Size int64 50 | Duration float64 51 | Qualitys []GetFileRespQuali 52 | Subtitles []GetFileRespSub 53 | Audios []GetFileRespAudio 54 | Tags []GetFileRespTag 55 | } 56 | 57 | func GetFile(LinkID uint, userID uint) (status int, fileData *GetFileResp, err error) { 58 | 59 | // query all files 60 | var link models.Link 61 | if res := inits.DB. 62 | Model(&models.Link{}). 63 | Preload("User"). 64 | Preload("Tags"). 65 | Preload("File"). 66 | Preload("File.Qualitys"). 67 | Preload("File.Subtitles"). 68 | Preload("File.Audios"). 69 | Where(&models.Link{ 70 | UserID: userID, 71 | }). 72 | First(&link, LinkID); res.Error != nil { 73 | return http.StatusNotFound, nil, echo.ErrNotFound 74 | } 75 | 76 | Qualitys := make([]GetFileRespQuali, 0) 77 | for _, Quality := range link.File.Qualitys { 78 | avgFps := Quality.AvgFrameRate 79 | if avgFps == 0 { 80 | avgFps = link.File.AvgFrameRate 81 | } 82 | Qualitys = append(Qualitys, GetFileRespQuali{ 83 | Name: Quality.Name, 84 | Type: Quality.Type, 85 | Height: Quality.Height, 86 | Width: Quality.Width, 87 | Size: Quality.Size, 88 | AvgFrameRate: avgFps, 89 | Ready: Quality.Ready, 90 | Progress: Quality.Progress * 100, 91 | Failed: Quality.Failed, 92 | }) 93 | } 94 | 95 | Subtitles := make([]GetFileRespSub, 0) 96 | for _, Subtitle := range link.File.Subtitles { 97 | Subtitles = append(Subtitles, GetFileRespSub{ 98 | Name: Subtitle.Name, 99 | Lang: Subtitle.Lang, 100 | Type: Subtitle.Type, 101 | Ready: Subtitle.Ready, 102 | }) 103 | } 104 | 105 | Audios := make([]GetFileRespAudio, 0) 106 | for _, Audio := range link.File.Audios { 107 | Audios = append(Audios, GetFileRespAudio{ 108 | Name: Audio.Name, 109 | Lang: Audio.Lang, 110 | Type: Audio.Type, 111 | Ready: Audio.Ready, 112 | }) 113 | } 114 | 115 | Tags := make([]GetFileRespTag, 0) 116 | for _, Tag := range link.Tags { 117 | Tags = append(Tags, GetFileRespTag{ 118 | ID: Tag.ID, 119 | Name: Tag.Name, 120 | }) 121 | } 122 | 123 | response := GetFileResp{ 124 | CreatedAt: *link.CreatedAt, 125 | UpdatedAt: *link.UpdatedAt, 126 | ID: link.ID, 127 | UUID: link.UUID, 128 | Name: link.Name, 129 | Thumbnail: fmt.Sprintf("%s/%s/image/thumb/%s", config.ENV.FolderVideoQualitysPub, link.UUID, link.File.Thumbnail), 130 | ParentFolderID: link.ParentFolderID, 131 | Size: link.File.Size, 132 | Duration: link.File.Duration, 133 | Qualitys: Qualitys, 134 | Subtitles: Subtitles, 135 | Audios: Audios, 136 | Tags: Tags, 137 | } 138 | 139 | return http.StatusOK, &response, nil 140 | } 141 | -------------------------------------------------------------------------------- /logic/GetFileExample.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "net/http" 6 | ) 7 | 8 | func GetFileExample() (status int, response string, err error) { 9 | return http.StatusOK, config.ENV.ProjectExampleVideo, nil 10 | } 11 | -------------------------------------------------------------------------------- /logic/GetM3u8Data.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "net/http" 9 | ) 10 | 11 | type GetM3u8DataRequest struct { 12 | UUID string `validate:"required,uuid_rfc4122"` 13 | AUDIOUUID string `validate:"required,uuid_rfc4122"` 14 | JWT string `validate:"required,jwt"` 15 | } 16 | 17 | type GetM3u8DataRequestMuted struct { 18 | UUID string `validate:"required,uuid_rfc4122"` 19 | JWT string `validate:"required,jwt"` 20 | } 21 | 22 | func GetM3u8Data(UUID string, AUDIOUUID string, JWT string) (status int, m3u8Str *string, err error) { 23 | //translate link id to file id 24 | var dbLink models.Link 25 | if dbRes := inits.DB. 26 | Model(&models.Link{}). 27 | Preload("File"). 28 | Preload("File.Qualitys"). 29 | Preload("File.Audios"). 30 | Where(&models.Link{ 31 | UUID: UUID, 32 | }). 33 | First(&dbLink); dbRes.Error != nil { 34 | return http.StatusNotFound, nil, errors.New("link doesn't exist") 35 | } 36 | 37 | //check if contains audio 38 | var dbAudioPtr *models.Audio 39 | if AUDIOUUID != "" { 40 | for _, audio := range dbLink.File.Audios { 41 | if audio.Ready && 42 | audio.UUID == AUDIOUUID && 43 | audio.Type == "hls" { 44 | dbAudioPtr = &audio 45 | break 46 | } 47 | } 48 | } 49 | m3u8Response := helpers.GenM3u8Stream(&dbLink, &dbLink.File.Qualitys, dbAudioPtr, JWT) 50 | return http.StatusOK, &m3u8Response, nil 51 | } 52 | -------------------------------------------------------------------------------- /logic/GetSubtitleData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | func GetSubtitleData(fileName string, UUID string, SUBUUID string) (status int, filePath *string, err error) { 14 | reFILE := regexp.MustCompile(`^out\.(ass)$`) 15 | 16 | if !reFILE.MatchString(fileName) { 17 | return http.StatusBadRequest, nil, errors.New("bad file format") 18 | } 19 | 20 | //translate link id to file id 21 | var dbLink models.Link 22 | 23 | if dbRes := inits.DB. 24 | Model(&models.Link{}). 25 | Preload("File"). 26 | Preload("File.Subtitles"). 27 | Where(&models.Link{ 28 | UUID: UUID, 29 | }). 30 | First(&dbLink); dbRes.Error != nil { 31 | return http.StatusNotFound, nil, errors.New("subtitle doesn't exist") 32 | } 33 | 34 | //check if subtitle uuid exists 35 | subExists := false 36 | for _, sub := range dbLink.File.Subtitles { 37 | if sub.Ready && 38 | sub.UUID == SUBUUID { 39 | subExists = true 40 | } 41 | } 42 | if !subExists { 43 | return http.StatusNotFound, nil, errors.New("subtitle doesn't exist") 44 | } 45 | 46 | fileRes := fmt.Sprintf("%s/%s/%s/%s", config.ENV.FolderVideoQualitysPriv, dbLink.File.UUID, SUBUUID, fileName) 47 | 48 | return http.StatusOK, &fileRes, nil 49 | } 50 | -------------------------------------------------------------------------------- /logic/GetThumbnailData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | func GetThumbnailData(fileName string, UUID string) (status int, filePath *string, err error) { 14 | reFILE := regexp.MustCompile(`^[1-4]x[1-4]\.(webp)$`) 15 | 16 | if !reFILE.MatchString(fileName) { 17 | return http.StatusBadRequest, nil, errors.New("bad file format") 18 | } 19 | 20 | //translate link id to file id 21 | var dbLink models.Link 22 | if dbRes := inits.DB. 23 | Model(&models.Link{}). 24 | Preload("File"). 25 | Where(&models.Link{ 26 | UUID: UUID, 27 | }). 28 | First(&dbLink); dbRes.Error != nil { 29 | return http.StatusNotFound, nil, errors.New("thumbnail doesn't exist") 30 | } 31 | 32 | fileRes := fmt.Sprintf("%s/%s/%s", config.ENV.FolderVideoQualitysPriv, dbLink.File.UUID, fileName) 33 | 34 | return http.StatusOK, &fileRes, nil 35 | } 36 | -------------------------------------------------------------------------------- /logic/GetVideoData.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "regexp" 11 | ) 12 | 13 | func GetVideoData(fileName string, qualityName string, UUID string) (status int, filePath *string, err error) { 14 | reQUALITY := regexp.MustCompile(`^([0-9]{3,4}p|(h264))$`) 15 | reFILE := regexp.MustCompile(`^out[0-9]{0,4}\.(m3u8|ts|webm|mp4)$`) 16 | 17 | if !reQUALITY.MatchString(qualityName) { 18 | return http.StatusBadRequest, nil, errors.New("bad quality format") 19 | } 20 | 21 | if !reFILE.MatchString(fileName) { 22 | return http.StatusBadRequest, nil, errors.New("bad file format") 23 | } 24 | 25 | //translate link id to file id 26 | var dbLink models.Link 27 | if dbRes := inits.DB. 28 | Model(&models.Link{}). 29 | Preload("File"). 30 | Where(&models.Link{ 31 | UUID: UUID, 32 | }). 33 | First(&dbLink); dbRes.Error != nil { 34 | return http.StatusNotFound, nil, errors.New("video doesn't exist") 35 | } 36 | 37 | fileRes := fmt.Sprintf("%s/%s/%s/%s", config.ENV.FolderVideoQualitysPriv, dbLink.File.UUID, qualityName, fileName) 38 | return http.StatusOK, &fileRes, nil 39 | } 40 | -------------------------------------------------------------------------------- /logic/HashCloneFile.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | func CloneFileByHash(fromHash string, toFolder uint, fileName string, userId uint) (status int, newFile *models.Link, err error) { 14 | // check if requested folder exists (if set) 15 | if toFolder > 0 { 16 | res := inits.DB.First(&models.Folder{}, toFolder) 17 | if res.Error != nil { 18 | return http.StatusBadRequest, nil, errors.New("parent folder doesn't exist") 19 | } 20 | } 21 | 22 | // check file hash with database 23 | var existingFile models.File 24 | if res := inits.DB. 25 | Where(&models.File{ 26 | Hash: fromHash, 27 | }).First(&existingFile); res.Error != nil { 28 | return http.StatusNotFound, nil, errors.New("requested hash doesnt match any file") 29 | } 30 | 31 | // file is dublicate and can be linked 32 | // link old uploaded file to new link 33 | dbLink := models.Link{ 34 | UUID: uuid.NewString(), 35 | ParentFolderID: toFolder, 36 | UserID: userId, 37 | FileID: existingFile.ID, 38 | Name: fileName, 39 | } 40 | if res := inits.DB.Create(&dbLink); res.Error != nil { 41 | log.Printf("Error saving link in database: %v", res.Error) 42 | return http.StatusInternalServerError, nil, res.Error 43 | } 44 | return http.StatusOK, &dbLink, nil 45 | } 46 | -------------------------------------------------------------------------------- /logic/ListFiles.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func ListFiles(fromFolder uint, userId uint) (status int, response *[]models.Link, err error) { 14 | //check if requested folder exists 15 | if fromFolder > 0 { 16 | res := inits.DB.First(&models.Folder{}, fromFolder) 17 | if res.Error != nil { 18 | return http.StatusBadRequest, nil, errors.New("parent folder doesn't exist") 19 | } 20 | } 21 | 22 | // query all files 23 | var links []models.Link 24 | res := inits.DB. 25 | Model(&models.Link{}). 26 | Preload("User"). 27 | Preload("File"). 28 | Preload("Tags"). 29 | Where(&models.Link{ 30 | ParentFolderID: fromFolder, 31 | UserID: userId, 32 | }, "ParentFolderID", "UserID"). 33 | Order("name ASC"). 34 | Find(&links) 35 | if res.Error != nil { 36 | log.Printf("Failed to query file list: %v", res.Error) 37 | return http.StatusInternalServerError, nil, echo.ErrInternalServerError 38 | } 39 | 40 | return http.StatusOK, &links, nil 41 | } 42 | -------------------------------------------------------------------------------- /logic/Thumbnail.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func CreateThumbnail(imageCountAxis int, inputFile string, height int, outputFile string, outputFolder string, videoDuration float64, fps float64) (status int, err error) { 16 | // read file & folder 17 | absOutputFolder, err := filepath.Abs(outputFolder) 18 | if err != nil { 19 | return http.StatusBadRequest, err 20 | } 21 | os.MkdirAll(absOutputFolder, 0777) 22 | 23 | absInputFile, err := filepath.Abs(inputFile) 24 | if err != nil { 25 | return http.StatusBadRequest, err 26 | } 27 | 28 | // build ffmpeg command 29 | imageCount := imageCountAxis * imageCountAxis 30 | imageSingeHeight := int(math.RoundToEven(float64(height/imageCountAxis)/2) * 2) 31 | imageFullHeight := imageSingeHeight * imageCountAxis 32 | 33 | ffmpegCommand := fmt.Sprintf("ffmpeg -i %s -vframes 1 ", absInputFile) 34 | filterComplex := `-filter_complex "` 35 | 36 | // filter complex overlay 37 | filterComplexStackPositionX := 0 38 | filterComplexStackPositionY := 0 39 | filterComplexStackPositionM := imageCountAxis - 1 40 | for i := 0; i < imageCount; i++ { 41 | videoStartTimeFrame := math.Floor((videoDuration / float64(imageCount+1)) * float64(i+1) * math.Floor(fps)) 42 | filterComplex += fmt.Sprintf( 43 | "[0:v]select='eq(n,%.0f)',scale=iw/%d:-1[X%dY%d];", 44 | videoStartTimeFrame, 45 | imageCountAxis, 46 | filterComplexStackPositionX, 47 | filterComplexStackPositionY, 48 | ) 49 | filterComplexStackPositionX++ 50 | // this will check if the next filterComplexStackPositionX is over the limit and set the new counter 51 | if filterComplexStackPositionX > filterComplexStackPositionM { 52 | filterComplexStackPositionX = 0 53 | filterComplexStackPositionY++ 54 | } 55 | } 56 | // add left to right 57 | for i := 0; i < imageCountAxis; i++ { 58 | inputs := "" 59 | for ii := 0; ii < imageCountAxis; ii++ { 60 | inputs += fmt.Sprintf("[X%dY%d]", ii, i) 61 | } 62 | filterComplex += fmt.Sprintf("%shstack=inputs=%d[R%d];", inputs, imageCountAxis, i) 63 | } 64 | 65 | // add top to bottom 66 | inputs := "" 67 | for i := 0; i < imageCountAxis; i++ { 68 | inputs += fmt.Sprintf("[R%d]", i) 69 | } 70 | filterComplex += fmt.Sprintf(`%svstack=inputs=%d" `, inputs, imageCountAxis) 71 | 72 | ffmpegCommand += filterComplex 73 | 74 | ffmpegCommand += fmt.Sprintf("%s/%s -y", absOutputFolder, outputFile) 75 | 76 | ffmpegCommandSimpleImage := fmt.Sprintf( 77 | `ffmpeg -i %s -ss %.2f -vf scale=-1:%d -vframes 1 %s/%s -y`, 78 | absInputFile, 79 | videoDuration/2, 80 | imageFullHeight, 81 | absOutputFolder, 82 | outputFile, 83 | ) 84 | 85 | cmd := exec.Command( 86 | "bash", 87 | "-c", 88 | ffmpegCommand, 89 | ) 90 | 91 | if err := cmd.Run(); err != nil { 92 | log.Printf("Failed during thumbnail conversion: %s", ffmpegCommand) 93 | 94 | // if tiles fail try simple one instead 95 | cmd := exec.Command( 96 | "bash", 97 | "-c", 98 | ffmpegCommandSimpleImage, 99 | ) 100 | if err := cmd.Run(); err != nil { 101 | log.Printf("Failed during simple thumbnail conversion: %v : %s", err, ffmpegCommandSimpleImage) 102 | return http.StatusInternalServerError, echo.ErrInternalServerError 103 | } 104 | 105 | } 106 | 107 | return http.StatusOK, nil 108 | } 109 | -------------------------------------------------------------------------------- /logic/UpdateWebhook.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "errors" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func UpdateWebhook(validated *models.WebhookUpdateValidation, userID uint) (status int, response string, err error) { 15 | 16 | var webhook models.Webhook 17 | if res := inits.DB.Where(&models.Webhook{ 18 | UserID: userID, 19 | }).First(&webhook, validated.WebhookID); res.Error != nil { 20 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 21 | return http.StatusNotFound, "", echo.ErrNotFound 22 | } 23 | log.Printf("Failed to query webhook %v: %v", validated.WebhookID, res.Error) 24 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 25 | } 26 | 27 | webhook.Name = validated.Name 28 | webhook.Url = validated.Url 29 | webhook.Rpm = validated.Rpm 30 | webhook.ReqQuery = validated.ReqQuery 31 | webhook.ResField = validated.ResField 32 | 33 | if res := inits.DB.Save(&webhook); res.Error != nil { 34 | log.Printf("Failed to update webhook %v: %v", validated.WebhookID, res.Error) 35 | return http.StatusInternalServerError, "", echo.ErrInternalServerError 36 | } 37 | 38 | return http.StatusOK, "ok", nil 39 | } 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ch/kirari04/videocms/cmd" 5 | "fmt" 6 | 7 | "github.com/thatisuday/commando" 8 | ) 9 | 10 | func main() { 11 | commando. 12 | SetExecutableName("videocms"). 13 | SetVersion("v1.0.0"). 14 | SetDescription("Videocms cli - To manage your instances.") 15 | 16 | commando. 17 | Register(nil). 18 | SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { 19 | fmt.Println("try the help command") 20 | }) 21 | 22 | commando. 23 | Register("serve:main"). 24 | SetShortDescription("starts the main server"). 25 | SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { 26 | cmd.ServeMain() 27 | }) 28 | 29 | commando. 30 | Register("config"). 31 | SetShortDescription("prints the current configuration"). 32 | SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { 33 | cmd.Config() 34 | }) 35 | 36 | commando. 37 | Register("create:user"). 38 | SetShortDescription("creates a new user"). 39 | SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { 40 | cmd.CreateUser() 41 | }) 42 | 43 | commando. 44 | Register("delete:user"). 45 | SetShortDescription("delete a user"). 46 | SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) { 47 | cmd.DeleteUser() 48 | }) 49 | 50 | commando.Parse(nil) 51 | } 52 | -------------------------------------------------------------------------------- /middlewares/Auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func Auth() echo.MiddlewareFunc { 12 | return echo.MiddlewareFunc(func(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) error { 14 | bearer := c.Request().Header.Get("Authorization") 15 | if bearer == "" { 16 | return c.String(http.StatusForbidden, "No JWT Token") 17 | } 18 | bearerHeader := strings.Split(bearer, " ") 19 | tokenString := bearerHeader[len(bearerHeader)-1] 20 | token, claims, err := auth.VerifyJWT(tokenString) 21 | if err != nil { 22 | return c.String(http.StatusForbidden, "Invalid JWT Token") 23 | } 24 | if !token.Valid { 25 | return c.String(http.StatusForbidden, "Expired JWT Token") 26 | } 27 | c.Set("Username", claims.Username) 28 | c.Set("UserID", claims.UserID) 29 | c.Set("Admin", claims.Admin) 30 | return next(c) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /middlewares/IsAdmin.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func IsAdmin() echo.MiddlewareFunc { 10 | return echo.MiddlewareFunc(func(next echo.HandlerFunc) echo.HandlerFunc { 11 | return func(c echo.Context) error { 12 | isAdmin, ok := c.Get("Admin").(bool) 13 | if !ok { 14 | c.Logger().Error("Failed to catch Admin") 15 | return c.NoContent(http.StatusInternalServerError) 16 | } 17 | if !isAdmin { 18 | return c.String(http.StatusForbidden, "Not Permitted") 19 | } 20 | return next(c) 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /middlewares/JwtStream.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "ch/kirari04/videocms/auth" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func JwtStream() echo.MiddlewareFunc { 11 | return echo.MiddlewareFunc(func(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(c echo.Context) error { 13 | uuid := c.Param("UUID") 14 | if uuid == "" { 15 | return c.String(http.StatusBadRequest, "Missing UUID parameter") 16 | } 17 | tknStr := c.QueryParam("jwt") 18 | if tknStr == "" { 19 | tknStr = c.Param("JWT") 20 | if tknStr == "" { 21 | return c.String(http.StatusBadRequest, "UUID parameter match issue") 22 | } 23 | } 24 | token, claims, err := auth.VerifyJWTStream(tknStr) 25 | if err != nil { 26 | return c.String(http.StatusForbidden, "Broken JWT") 27 | } 28 | if !token.Valid { 29 | return c.String(http.StatusForbidden, "Invalid JWT") 30 | } 31 | if claims.UUID != uuid { 32 | return c.String(http.StatusForbidden, "Mismacht UUID") 33 | } 34 | return next(c) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /models/Audio.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Audio struct { 6 | Model 7 | UUID string 8 | Name string `gorm:"size:120;"` 9 | Lang string `gorm:"size:10;"` 10 | Path string `gorm:"size:120;" json:"-"` 11 | OriginalCodec string `json:"-"` 12 | Index int 13 | Codec string 14 | Type string 15 | OutputFile string `json:"-"` 16 | Encoding bool 17 | Progress float64 18 | Failed bool 19 | Ready bool `json:"-"` 20 | Error string `json:"-"` 21 | File File `json:"-"` 22 | FileID uint 23 | } 24 | 25 | func (c *Audio) SetProcess(v float64) { 26 | c.Progress = v 27 | } 28 | func (c *Audio) GetProcess() float64 { 29 | return c.Progress 30 | } 31 | func (c *Audio) Save(DB *gorm.DB) *gorm.DB { 32 | return DB.Save(c) 33 | } 34 | 35 | type AudioGetValidation struct { 36 | UUID string `validate:"required,uuid_rfc4122" param:"UUID"` 37 | AUDIOUUID string `validate:"required,uuid_rfc4122" param:"AUDIOUUID"` 38 | FILE string `validate:"required" param:"FILE"` 39 | } 40 | 41 | type AvailableAudio struct { 42 | Type string // hls | ogg | mp3 43 | Codec string 44 | OutputFile string 45 | } 46 | 47 | var AvailableAudios = []AvailableAudio{ 48 | { 49 | Type: "hls", 50 | Codec: "aac", 51 | OutputFile: "audio.m3u8", 52 | }, 53 | // { 54 | // Type: "ogg", 55 | // Codec: "libopus", 56 | // OutputFile: "audio.ogg", 57 | // }, 58 | // { 59 | // Type: "mp3", 60 | // Codec: "libmp3lame", 61 | // OutputFile: "audio.mp3", 62 | // }, 63 | } 64 | -------------------------------------------------------------------------------- /models/File.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type File struct { 4 | Model 5 | UUID string 6 | Hash string `gorm:"size:128;" json:"-"` 7 | Thumbnail string 8 | Size int64 9 | Duration float64 10 | AvgFrameRate float64 11 | Height int64 12 | Width int64 13 | Path string `gorm:"size:120;" json:"-"` 14 | Folder string `gorm:"size:120;" json:"-"` 15 | User User `json:"-"` 16 | UserID uint 17 | Qualitys []Quality `json:"-"` 18 | Subtitles []Subtitle `json:"-"` 19 | Audios []Audio `json:"-"` 20 | Links []Link `json:"-"` 21 | } 22 | 23 | type FileCreateValidation struct { 24 | Name string `validate:"required,min=1,max=120"` 25 | ParentFolderID uint `validate:"number"` 26 | } 27 | 28 | type FileCloneValidation struct { 29 | Name string `validate:"required,min=1,max=120"` 30 | Sha256 string `validate:"required,sha256"` 31 | ParentFolderID uint `validate:"number"` 32 | } 33 | -------------------------------------------------------------------------------- /models/Folder.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Folder struct { 4 | Model 5 | Name string `gorm:"size:120;"` 6 | User User `json:"-"` 7 | UserID uint 8 | ParentFolder *Folder `json:"-"` 9 | ParentFolderID uint 10 | } 11 | 12 | type FolderCreateValidation struct { 13 | Name string `validate:"required,min=1,max=120" json:"Name" form:"Name"` 14 | ParentFolderID uint `validate:"number" json:"ParentFolderID" form:"ParentFolderID"` 15 | } 16 | 17 | type FolderListValidation struct { 18 | ParentFolderID uint `validate:"number" query:"ParentFolderID"` 19 | } 20 | 21 | type FolderDeleteValidation struct { 22 | FolderID uint `validate:"required,number" json:"FolderID" form:"FolderID"` 23 | } 24 | type FoldersDeleteValidation struct { 25 | FolderIDs []FolderDeleteValidation `validate:"required" json:"FolderIDs" form:"FolderIDs"` 26 | } 27 | 28 | type FolderUpdateValidation struct { 29 | Name string `validate:"required,min=1,max=120" json:"Name" form:"Name"` 30 | FolderID uint `validate:"required,number" json:"FolderID" form:"FolderID"` 31 | ParentFolderID uint `validate:"number" json:"ParentFolderID" form:"ParentFolderID"` 32 | } 33 | -------------------------------------------------------------------------------- /models/Link.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Link struct { 4 | Model 5 | UUID string 6 | Name string `gorm:"size:128;"` 7 | File File `json:"-"` 8 | FileID uint `json:"-"` 9 | User User `json:"-"` 10 | UserID uint `json:"-"` 11 | ParentFolder *Folder `json:"-"` 12 | ParentFolderID uint 13 | Tags []*Tag `gorm:"many2many:tag_links;"` 14 | } 15 | 16 | type LinkListValidation struct { 17 | ParentFolderID uint `validate:"number" query:"ParentFolderID"` 18 | } 19 | 20 | type LinkDeleteValidation struct { 21 | LinkID uint `validate:"required,number" form:"LinkID"` 22 | } 23 | 24 | type LinkUpdateValidation struct { 25 | LinkID uint `validate:"required,number" form:"LinkID"` 26 | Name string `validate:"required,min=1,max=120" form:"Name"` 27 | ParentFolderID uint `validate:"number" form:"ParentFolderID"` 28 | } 29 | 30 | type LinksDeleteValidation struct { 31 | LinkIDs []LinkDeleteValidation `validate:"required"` 32 | } 33 | 34 | type LinkGetValidation struct { 35 | LinkID uint `validate:"required,number" query:"LinkID"` 36 | } 37 | -------------------------------------------------------------------------------- /models/Model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Model struct { 10 | ID uint `gorm:"primarykey"` 11 | CreatedAt *time.Time 12 | UpdatedAt *time.Time 13 | DeletedAt *gorm.DeletedAt `gorm:"index"` 14 | } 15 | -------------------------------------------------------------------------------- /models/Quality.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Quality struct { 8 | Model 9 | Name string `gorm:"size:20;"` 10 | Height int64 11 | Width int64 12 | Size int64 13 | // Crf int `json:"-"` 14 | 15 | VideoBitrate string // example: 8000k 16 | AudioBitrate string // example: 128k 17 | Profile string // example: high 18 | Level string // example: 5.2 | 5.1 | 4.2 | 4.0 | 3.1 | 3.0 19 | CodecStringAVC string // example: avc1.640034 20 | 21 | Type string 22 | Muted bool 23 | AudioCodec string `json:"-"` 24 | AvgFrameRate float64 25 | Path string `gorm:"size:120;" json:"-"` 26 | OutputFile string 27 | Encoding bool 28 | Progress float64 29 | Failed bool 30 | Ready bool `json:"-"` 31 | Error string `json:"-"` 32 | File File `json:"-"` 33 | FileID uint 34 | } 35 | 36 | func (c *Quality) SetProcess(v float64) { 37 | c.Progress = v 38 | } 39 | func (c *Quality) GetProcess() float64 { 40 | return c.Progress 41 | } 42 | func (c *Quality) Save(DB *gorm.DB) *gorm.DB { 43 | return DB.Save(c) 44 | } 45 | 46 | type AvailableQuality struct { 47 | Name string 48 | FolderName string 49 | Height int64 50 | Width int64 51 | // Crf int 52 | VideoBitrate string // example: 8000k 53 | AudioBitrate string // example: 128k 54 | Profile string // example: high 55 | Level string // example: 5.2| 5.1 | 4.2 | 4.0 | 3.1 | 3.0 56 | CodecStringAVC string // example: avc1.640034 57 | 58 | Type string // hls 59 | Muted bool 60 | OutputFile string 61 | Enabled bool 62 | } 63 | 64 | var AvailableQualitys []AvailableQuality 65 | -------------------------------------------------------------------------------- /models/Setting.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SettingValidation struct { 4 | ID uint `validate:"required,number"` 5 | Setting 6 | } 7 | 8 | type Setting struct { 9 | Model 10 | AppName string `validate:"required,min=1,max=120" gorm:"size:120;"` 11 | BaseUrl string `validate:"required,min=1,max=255" gorm:"size:255;"` 12 | 13 | Project string `validate:"required,min=1,max=512" gorm:"size:512;"` 14 | ProjectDocumentation string `validate:"required,min=1,max=512" gorm:"size:512;"` 15 | ProjectDownload string `validate:"required,min=1,max=512" gorm:"size:512;"` 16 | ProjectExampleVideo string `validate:"required,min=1,max=512" gorm:"size:512;"` 17 | 18 | JwtSecretKey string `validate:"required,min=8,max=512" gorm:"size:512;"` 19 | JwtUploadSecretKey string `validate:"required,min=8,max=512" gorm:"size:512;"` 20 | 21 | ReloadHtml string `validate:"required,boolean"` 22 | EncodingEnabled string `validate:"required,boolean"` 23 | UploadEnabled string `validate:"required,boolean"` 24 | RatelimitEnabled string `validate:"required,boolean"` 25 | CloudflareEnabled string `validate:"required,boolean"` 26 | 27 | MaxItemsMultiDelete string `validate:"required,number,min=1"` 28 | MaxRunningEncodes string `validate:"required,number,min=1"` 29 | 30 | MaxUploadFilesize string `validate:"required,number,min=1"` 31 | MaxUploadChuncksize string `validate:"required,number,min=1"` 32 | MaxUploadSessions string `validate:"required,number,min=1"` 33 | MaxPostSize string `validate:"required,number,min=1"` 34 | 35 | CorsAllowOrigins string `validate:"required,min=1,max=1000" gorm:"size:1000;"` 36 | CorsAllowHeaders string `validate:"required,min=1,max=1000" gorm:"size:1000;"` 37 | CorsAllowCredentials string `validate:"required,boolean"` 38 | 39 | CaptchaEnabled string `validate:"required,boolean"` 40 | CaptchaType string `validate:"required_if=CaptchaEnabled 1,omitempty,min=1,max=10" gorm:"size:10;"` 41 | Captcha_Recaptcha_PrivateKey string `validate:"required_if=CaptchaType recaptcha,omitempty,min=1,max=40" gorm:"size:40;"` 42 | Captcha_Recaptcha_PublicKey string `validate:"required_if=CaptchaType recaptcha,omitempty,min=1,max=40" gorm:"size:40;"` 43 | Captcha_Hcaptcha_PrivateKey string `validate:"required_if=CaptchaType hcaptcha,omitempty,min=1,max=42" gorm:"size:42;"` 44 | Captcha_Hcaptcha_PublicKey string `validate:"required_if=CaptchaType hcaptcha,omitempty,uuid_rfc4122"` 45 | 46 | EncodeHls240p string `validate:"required,boolean"` 47 | Hls240pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 48 | EncodeHls360p string `validate:"required,boolean"` 49 | Hls360pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 50 | EncodeHls480p string `validate:"required,boolean"` 51 | Hls480pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 52 | EncodeHls720p string `validate:"required,boolean"` 53 | Hls720pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 54 | EncodeHls1080p string `validate:"required,boolean"` 55 | Hls1080pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 56 | EncodeHls1440p string `validate:"required,boolean"` 57 | Hls1440pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 58 | EncodeHls2160p string `validate:"required,boolean"` 59 | Hls2160pVideoBitrate string `validate:"required,min=1,max=7" gorm:"size:7;"` 60 | 61 | PluginPgsServer string `validate:"required"` 62 | EnablePluginPgsServer string `validate:"required,boolean"` 63 | 64 | DownloadEnabled string `validate:"required,boolean"` 65 | } 66 | -------------------------------------------------------------------------------- /models/Subtitle.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Subtitle struct { 6 | Model 7 | UUID string 8 | Name string `gorm:"size:120;"` 9 | Lang string `gorm:"size:10;"` 10 | Path string `gorm:"size:120;" json:"-"` 11 | OriginalCodec string `json:"-"` 12 | Index int `json:"-"` 13 | Codec string 14 | Type string 15 | OutputFile string 16 | Encoding bool 17 | Progress float64 18 | Failed bool 19 | Ready bool 20 | Error string `json:"-"` 21 | File File `json:"-"` 22 | FileID uint 23 | } 24 | 25 | func (c *Subtitle) SetProcess(v float64) { 26 | c.Progress = v 27 | } 28 | func (c *Subtitle) GetProcess() float64 { 29 | return c.Progress 30 | } 31 | func (c *Subtitle) Save(DB *gorm.DB) *gorm.DB { 32 | return DB.Save(c) 33 | } 34 | 35 | type AvailableSubtitle struct { 36 | Type string // ass | vtt 37 | Codec string 38 | OutputFile string 39 | } 40 | 41 | var AvailableSubtitles = []AvailableSubtitle{ 42 | { 43 | Type: "ass", 44 | Codec: "ass", 45 | OutputFile: "out.ass", 46 | }, 47 | { 48 | Type: "vtt", 49 | Codec: "webvtt", 50 | OutputFile: "out.vtt", 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /models/SystemResource.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SystemResource struct { 4 | Model 5 | Cpu float64 6 | Mem float64 7 | NetOut uint64 8 | NetIn uint64 9 | DiskW uint64 10 | DiskR uint64 11 | ENCQualityQueue int64 12 | ENCAudioQueue int64 13 | ENCSubtitleQueue int64 14 | } 15 | 16 | type SystemResourceGetValidation struct { 17 | Interval string `query:"interval" validate:"required,oneof=5min 1h 7h"` 18 | } 19 | -------------------------------------------------------------------------------- /models/Tag.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Tag struct { 10 | Model 11 | Name string `gorm:"size:128;"` 12 | UserId uint `gorm:"index" json:"-"` 13 | User User `json:"-"` 14 | Links []*Link `gorm:"many2many:tag_links;" json:"-"` 15 | } 16 | 17 | type TagLinks struct { 18 | LinkID uint `gorm:"primaryKey"` 19 | TagID uint `gorm:"primaryKey"` 20 | CreatedAt time.Time 21 | DeletedAt gorm.DeletedAt 22 | } 23 | 24 | type TagCreateValidation struct { 25 | Name string `validate:"required,min=1,max=120" json:"Name" form:"Name"` 26 | LinkId uint `validate:"required,number" json:"LinkId" form:"LinkId"` 27 | } 28 | 29 | type TagDeleteValidation struct { 30 | TagId uint `validate:"required,number" json:"TagId" form:"TagId"` 31 | LinkId uint `validate:"required,number" json:"LinkId" form:"LinkId"` 32 | } 33 | -------------------------------------------------------------------------------- /models/UploadChunck.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type UploadChunck struct { 4 | Model 5 | Index uint 6 | Path string `gorm:"size:120;" json:"-"` 7 | UploadSession UploadSession 8 | UploadSessionID uint 9 | } 10 | 11 | type UploadChunckValidation struct { 12 | Index *uint `validate:"required,min=0,max=10000" json:"Index" form:"Index"` 13 | SessionJwtToken string `validate:"required,jwt" json:"SessionJwtToken" form:"SessionJwtToken"` 14 | } 15 | -------------------------------------------------------------------------------- /models/UploadFile.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type UploadFileValidation struct { 4 | SessionJwtToken string `validate:"required,min=1" json:"SessionJwtToken" form:"SessionJwtToken"` 5 | } 6 | -------------------------------------------------------------------------------- /models/UploadSession.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/golang-jwt/jwt/v5" 4 | 5 | type UploadSession struct { 6 | Model 7 | Name string `gorm:"size:128;"` 8 | UUID string 9 | Hash string `gorm:"size:128;" json:"-"` 10 | Size int64 11 | ChunckCount int 12 | SessionFolder string `gorm:"size:120;" json:"-"` 13 | ParentFolder *Folder `json:"-"` 14 | ParentFolderID uint `json:"-"` 15 | User User `json:"-"` 16 | UserID uint 17 | UploadChuncks []UploadChunck `json:"-"` 18 | } 19 | 20 | type UploadSessionClaims struct { 21 | UUID string `json:"uuid"` 22 | UserID uint `json:"userid"` 23 | jwt.RegisteredClaims 24 | } 25 | 26 | type UploadSessionValidation struct { 27 | Name string `validate:"required,min=1,max=128" json:"Name" form:"Name"` 28 | Size int64 `validate:"required,number,min=1" json:"Size" form:"Size"` 29 | ParentFolderID uint `validate:"number" json:"ParentFolderID" form:"ParentFolderID"` 30 | } 31 | 32 | type DeleteUploadSessionValidation struct { 33 | UploadSessionUUID string `validate:"required,uuid_rfc4122" json:"UploadSessionUUID" form:"UploadSessionUUID"` 34 | } 35 | -------------------------------------------------------------------------------- /models/User.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Model 5 | Username string `gorm:"unique;size:32;"` 6 | Hash string `gorm:"size:250;" json:"-"` 7 | Admin bool 8 | Email string 9 | Balance float64 10 | Storage int64 11 | Settings UserSettings 12 | 13 | Folders []Folder `json:"-"` 14 | Webhooks []Webhook `json:"-"` 15 | } 16 | 17 | type UserLoginValidation struct { 18 | Username string `validate:"required,min=3,max=32" form:"username"` 19 | Password string `validate:"required,min=8,max=250" form:"password"` 20 | } 21 | 22 | type UserRegisterValidation struct { 23 | Username string `validate:"required,min=3,max=32"` 24 | Password string `validate:"required,min=8,max=250"` 25 | Admin *bool `validate:"required,boolean"` 26 | } 27 | -------------------------------------------------------------------------------- /models/UserSettings.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | type UserSettings struct { 11 | WebhooksEnabled bool 12 | WebhooksMax int 13 | UploadSessionsMax int64 14 | EnablePlayerCaptcha bool 15 | } 16 | 17 | // Scan scan value into Jsonb, implements sql.Scanner interface 18 | func (j *UserSettings) Scan(value interface{}) error { 19 | bytes, ok := value.([]byte) 20 | if !ok { 21 | return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 22 | } 23 | 24 | result := UserSettings{} 25 | err := json.Unmarshal(bytes, &result) 26 | *j = UserSettings(result) 27 | return err 28 | } 29 | 30 | // Value return json value, implement driver.Valuer interface 31 | func (j UserSettings) Value() (driver.Value, error) { 32 | v, err := json.Marshal(j) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return json.RawMessage(v).MarshalJSON() 37 | } 38 | 39 | type UserSettingsUpdateValidation struct { 40 | EnablePlayerCaptcha *bool `validate:"required,boolean"` 41 | } 42 | -------------------------------------------------------------------------------- /models/WebPage.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type WebPage struct { 4 | Model 5 | Path string `gorm:"size:50;"` 6 | Title string `gorm:"size:128;"` 7 | Html string `gorm:"size:50000;"` 8 | ListInFooter bool 9 | } 10 | 11 | type WebPageCreateValidation struct { 12 | Path string `validate:"required,min=2,max=50"` 13 | Title string `validate:"required,min=2,max=128"` 14 | Html string `validate:"required,min=0,max=50000"` 15 | ListInFooter *bool `validate:"required,boolean"` 16 | } 17 | 18 | type WebPageUpdateValidation struct { 19 | WebPageID uint `validate:"required,number"` 20 | Path string `validate:"required,min=2,max=50"` 21 | Title string `validate:"required,min=2,max=128"` 22 | Html string `validate:"required,min=0,max=50000"` 23 | ListInFooter *bool `validate:"required,boolean"` 24 | } 25 | 26 | type WebPageDeleteValidation struct { 27 | WebPageID uint `validate:"required,number"` 28 | } 29 | 30 | type WebPageGetValidation struct { 31 | Path string `validate:"required,min=2,max=50" query:"Path"` 32 | } 33 | -------------------------------------------------------------------------------- /models/Webhooks.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Webhook struct { 4 | Model 5 | Name string `gorm:"size:128;"` 6 | Url string `gorm:"size:255;"` 7 | Rpm int 8 | ReqQuery string 9 | ResField string 10 | 11 | User User `json:"-"` 12 | UserID uint 13 | } 14 | 15 | type WebhookListValidation struct { 16 | } 17 | 18 | type WebhookCreateValidation struct { 19 | Name string `validate:"required,min=1,max=120"` 20 | Url string `validate:"required,http_url,min=4,max=255"` 21 | Rpm int `validate:"required,number,min=1,max=60"` 22 | ReqQuery string `validate:"required,alpha,min=0,max=50"` 23 | ResField string `validate:"required,alpha,min=0,max=50"` 24 | } 25 | 26 | type WebhookUpdateValidation struct { 27 | WebhookID uint `validate:"required,number"` 28 | Name string `validate:"required,min=1,max=120"` 29 | Url string `validate:"required,http_url,min=4,max=255"` 30 | Rpm int `validate:"required,number,min=1,max=60"` 31 | ReqQuery string `validate:"required,alpha,min=0,max=50"` 32 | ResField string `validate:"required,alpha,min=0,max=50"` 33 | } 34 | 35 | type WebhookDeleteValidation struct { 36 | WebhookID uint `validate:"required,number"` 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/svgs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/logo-banner-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/public/logo-banner-big.png -------------------------------------------------------------------------------- /public/logo-banner-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/public/logo-banner-small.png -------------------------------------------------------------------------------- /public/logo-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/public/logo-banner.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/public/logo.png -------------------------------------------------------------------------------- /routes/api.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "ch/kirari04/videocms/controllers" 5 | "ch/kirari04/videocms/helpers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/middlewares" 8 | "time" 9 | 10 | "github.com/labstack/echo/v4/middleware" 11 | ) 12 | 13 | func Api() { 14 | inits.Api.Use(middleware.RateLimiterWithConfig(*helpers.LimiterConfig(10, 500, time.Minute*5))) 15 | 16 | auth := inits.Api.Group("/auth") 17 | auth.POST("/login", 18 | controllers.AuthLogin, 19 | middleware.RateLimiterWithConfig(*helpers.LimiterConfig(1, 2, time.Minute*5))) 20 | auth.GET("/check", 21 | controllers.AuthCheck, 22 | middleware.RateLimiterWithConfig(*helpers.LimiterConfig(1, 2, time.Minute*5))) 23 | auth.GET("/refresh", 24 | controllers.AuthRefresh, 25 | middleware.RateLimiterWithConfig(*helpers.LimiterConfig(1, 2, time.Minute*5))) 26 | auth.POST("/apikey", 27 | controllers.AuthApikey, 28 | middleware.RateLimiterWithConfig(*helpers.LimiterConfig(1, 2, time.Minute*5)), 29 | middlewares.Auth()) 30 | 31 | // Routes that dont require authentication 32 | inits.Api.GET("/config", controllers.GetConfig) 33 | inits.Api.GET("/file/example", controllers.GetFileExample) 34 | inits.Api.GET("/p/pages", controllers.ListPublicWebPage) 35 | inits.Api.GET("/p/page", controllers.GetPublicWebPage) 36 | 37 | // requires uploadsession jwt inside body 38 | inits.Api.POST("/pcu/chunck", controllers.CreateUploadChunck) 39 | 40 | // Routes that require to be authenticated 41 | protectedApi := inits.Api.Group("", middlewares.Auth()) 42 | protectedApi.POST("/folder", controllers.CreateFolder) 43 | protectedApi.PUT("/folder", controllers.UpdateFolder) 44 | protectedApi.DELETE("/folder", controllers.DeleteFolder) 45 | protectedApi.GET("/folders", controllers.ListFolders) 46 | protectedApi.DELETE("/folders", controllers.DeleteFolders) 47 | 48 | protectedApi.POST("/file", controllers.CreateFile) 49 | protectedApi.POST("/file/clone", controllers.CloneFile) 50 | protectedApi.GET("/file", controllers.GetFile) 51 | protectedApi.PUT("/file", controllers.UpdateFile) 52 | protectedApi.DELETE("/file", controllers.DeleteFileController) 53 | protectedApi.GET("/files", controllers.ListFiles) 54 | protectedApi.DELETE("/files", controllers.DeleteFilesController) 55 | protectedApi.POST("/file/tag", controllers.CreateTagController) 56 | protectedApi.DELETE("/file/tag", controllers.DeleteTagController) 57 | 58 | protectedApi.GET("/account", controllers.GetAccount) 59 | protectedApi.GET("/account/settings", controllers.GetUserSettingsController) 60 | protectedApi.PUT("/account/settings", controllers.UpdateUserSettingsController) 61 | 62 | protectedApi.GET("/pages", controllers.ListWebPage, middlewares.IsAdmin()) 63 | protectedApi.POST("/page", controllers.CreateWebPage, middlewares.IsAdmin()) 64 | protectedApi.PUT("/page", controllers.UpdateWebPage, middlewares.IsAdmin()) 65 | protectedApi.DELETE("/page", controllers.DeleteWebPage, middlewares.IsAdmin()) 66 | 67 | protectedApi.GET("/stats", controllers.GetSystemStats, middlewares.IsAdmin()) 68 | protectedApi.GET("/settings", controllers.GetSettings, middlewares.IsAdmin()) 69 | protectedApi.PUT("/settings", controllers.UpdateSettings, middlewares.IsAdmin()) 70 | 71 | protectedApi.GET("/users", controllers.GetUsers, middlewares.IsAdmin()) 72 | 73 | protectedApi.POST("/webhook", controllers.CreateWebhook) 74 | protectedApi.PUT("/webhook", controllers.UpdateWebhook) 75 | protectedApi.DELETE("/webhook", controllers.DeleteWebhook) 76 | protectedApi.GET("/webhooks", controllers.ListWebhooks) 77 | 78 | protectedApi.GET("/encodings", controllers.GetEncodingFiles) 79 | 80 | protectedApi.GET("/pcu/sessions", controllers.GetUploadSessions) 81 | protectedApi.POST("/pcu/session", controllers.CreateUploadSession) 82 | protectedApi.DELETE("/pcu/session", controllers.DeleteUploadSession) 83 | // protectedApi.Post("/pcu/chunck", controllers.CreateUploadChunck) 84 | protectedApi.POST("/pcu/file", controllers.CreateUploadFile) 85 | } 86 | -------------------------------------------------------------------------------- /routes/web.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/controllers" 6 | "ch/kirari04/videocms/inits" 7 | "ch/kirari04/videocms/middlewares" 8 | ) 9 | 10 | func Web() { 11 | inits.App.Static("/", "public/") 12 | 13 | inits.App.GET("/v/:UUID", controllers.PlayerController) 14 | 15 | videoData := inits.App.Group(config.ENV.FolderVideoQualitysPub) 16 | videoData.GET("/:UUID/stream/muted/master.m3u8", controllers.GetM3u8Data, middlewares.JwtStream()) 17 | videoData.GET("/:UUID/image/thumb/:FILE", controllers.GetThumbnailData) 18 | videoData.GET("/:UUID/:SUBUUID/subtitle/:FILE", controllers.GetSubtitleData, middlewares.JwtStream()) 19 | videoData.GET("/:UUID/:AUDIOUUID/stream/master.m3u8", controllers.GetM3u8Data, middlewares.JwtStream()) 20 | videoData.GET("/:UUID/:QUALITY/download/video.mkv", controllers.DownloadVideoController, middlewares.JwtStream()) 21 | videoData.GET("/:UUID/:QUALITY/:JWT/:STREAM/stream/video.mp4", controllers.DownloadVideoController, middlewares.JwtStream()) 22 | // no jwt stream 23 | videoData.GET("/:UUID/:QUALITY/:FILE", controllers.GetVideoData) 24 | videoData.GET("/:UUID/:AUDIOUUID/audio/:FILE", controllers.GetAudioData) 25 | } 26 | -------------------------------------------------------------------------------- /services/Deleter.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "ch/kirari04/videocms/inits" 5 | "ch/kirari04/videocms/models" 6 | "log" 7 | "os" 8 | "time" 9 | ) 10 | 11 | func Deleter() { 12 | for { 13 | runDeleter() 14 | time.Sleep(time.Second * 20) 15 | } 16 | } 17 | 18 | func runDeleter() { 19 | var notReferencedFiles []uint 20 | if res := inits.DB. 21 | Raw(` 22 | SELECT files.id FROM files 23 | JOIN links ON links.file_id = files.id 24 | GROUP BY files.id 25 | HAVING COUNT(links.id) = SUM(CASE WHEN links.deleted_at IS NULL THEN 0 ELSE 1 END); 26 | `).Scan(¬ReferencedFiles); res.Error != nil { 27 | log.Printf("Failed to query unreferenced files: %v", res.Error) 28 | return 29 | } 30 | 31 | if len(notReferencedFiles) > 0 { 32 | if res := inits.DB.Delete(&models.File{}, notReferencedFiles); res.Error != nil { 33 | log.Printf("Failed to delete unreferenced files: %v", res.Error) 34 | return 35 | } 36 | } 37 | 38 | var todos []models.File 39 | if res := inits.DB. 40 | Model(&models.File{}). 41 | Preload("Qualitys"). 42 | Preload("Subtitles"). 43 | Preload("Audios"). 44 | Preload("Links"). 45 | Unscoped(). 46 | Where("deleted_at IS NOT NULL"). 47 | Find(&todos, todos); res.Error != nil { 48 | log.Printf("Failed to query deleted files: %v", res.Error) 49 | return 50 | } 51 | 52 | if len(todos) > 0 { 53 | log.Printf("Queued %d file to delete", len(todos)) 54 | } 55 | var skippingDeletion int 56 | var successDeletion int 57 | for _, todo := range todos { 58 | /** 59 | * check if all files qualities, subs & audios are not currently encoding because else there might be 60 | * parallel to the delete command an active ffmpeg conversion running 61 | */ 62 | encoding := false 63 | for _, quality := range todo.Qualitys { 64 | if quality.Encoding { 65 | encoding = true 66 | } 67 | } 68 | for _, audio := range todo.Audios { 69 | if audio.Encoding { 70 | encoding = true 71 | } 72 | } 73 | for _, sub := range todo.Subtitles { 74 | if sub.Encoding { 75 | encoding = true 76 | } 77 | } 78 | 79 | if encoding { 80 | // kill ffmpeg process if active 81 | for _, v := range ActiveEncodings { 82 | if v.FileID == todo.ID && v.Channel != nil { 83 | *v.Channel <- true 84 | } 85 | } 86 | 87 | // we will try again in the next loop (the encoding process may be finished until then) 88 | skippingDeletion++ 89 | continue 90 | } 91 | 92 | // delete related stuff 93 | if res := inits.DB. 94 | Unscoped(). 95 | Where(&models.Subtitle{ 96 | FileID: todo.ID, 97 | }). 98 | Delete(&models.Subtitle{}); res.Error != nil { 99 | log.Printf("Failed to delete Subtitles from database: %v", res.Error) 100 | continue 101 | } 102 | 103 | if res := inits.DB. 104 | Unscoped(). 105 | Where(&models.Audio{ 106 | FileID: todo.ID, 107 | }). 108 | Delete(&models.Audio{}); res.Error != nil { 109 | log.Printf("Failed to delete Audios from database: %v", res.Error) 110 | continue 111 | } 112 | 113 | if res := inits.DB. 114 | Unscoped(). 115 | Where(&models.Quality{ 116 | FileID: todo.ID, 117 | }). 118 | Delete(&models.Quality{}); res.Error != nil { 119 | log.Printf("Failed to delete Qualities from database: %v", res.Error) 120 | continue 121 | } 122 | 123 | /** 124 | * First delete the original file (it might still exists if some error happend or it didn't finished encoding yet) 125 | * Then we can delete the folder too 126 | */ 127 | if todo.Path != "" { 128 | if stats, err := os.Stat(todo.Path); !os.IsNotExist(err) && !stats.IsDir() { 129 | if err := os.Remove(todo.Path); err != nil { 130 | log.Printf("Failed to delete original file: %v", err) 131 | } 132 | } 133 | } 134 | 135 | if stats, err := os.Stat(todo.Folder); !os.IsNotExist(err) && stats.IsDir() { 136 | if err := os.RemoveAll(todo.Folder); err != nil { 137 | log.Printf("Failed to delete folder of file: %v", err) 138 | } 139 | } 140 | 141 | // delete file from database 142 | if res := inits.DB. 143 | Unscoped(). 144 | Delete(&todo); res.Error != nil { 145 | log.Printf("Failed to delete File from database: %v", res.Error) 146 | continue 147 | } 148 | successDeletion++ 149 | } 150 | if skippingDeletion > 0 { 151 | log.Printf("Skipped %d files from deletion", skippingDeletion) 152 | } 153 | if successDeletion > 0 { 154 | log.Printf("Successfully deleted %d files", successDeletion) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /services/EncoderCleanup.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "ch/kirari04/videocms/helpers" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "os" 9 | "time" 10 | ) 11 | 12 | func EncoderCleanup() { 13 | for { 14 | runEncoderCleanup() 15 | time.Sleep(time.Minute) 16 | } 17 | } 18 | 19 | /* 20 | This function deletes the originally uploaded file after all qualitys and subtitles were encoded 21 | */ 22 | func runEncoderCleanup() { 23 | var dbReadyFiles []models.File 24 | if res := inits.DB. 25 | Preload("Qualitys"). 26 | Preload("Subtitles"). 27 | Preload("Audios"). 28 | Not(&models.File{ 29 | Path: "", 30 | }, "Path"). 31 | Find(&dbReadyFiles); res.Error != nil { 32 | log.Printf("Failed to get PossibleDeleteTargets: %v", res.Error) 33 | return 34 | } 35 | 36 | for _, dbReadyFile := range dbReadyFiles { 37 | var qualityAmount int64 38 | if res := inits.DB. 39 | Model(&models.Quality{}). 40 | Where(&models.Quality{ 41 | FileID: dbReadyFile.ID, 42 | Ready: true, 43 | }). 44 | Count(&qualityAmount); res.Error != nil { 45 | log.Printf("Failed to count quality by (delete candidate): Searcher ID %d inside database. Error: %v", dbReadyFile.ID, res.Error) 46 | continue 47 | } 48 | 49 | var subtitleAmount int64 50 | if res := inits.DB. 51 | Model(&models.Subtitle{}). 52 | Where(&models.Subtitle{ 53 | FileID: dbReadyFile.ID, 54 | Ready: true, 55 | }). 56 | Count(&subtitleAmount); res.Error != nil { 57 | log.Printf("Failed to count subtitle by (delete candidate): Searcher ID %d inside database. Error: %v", dbReadyFile.ID, res.Error) 58 | continue 59 | } 60 | 61 | var audioAmount int64 62 | if res := inits.DB. 63 | Model(&models.Audio{}). 64 | Where(&models.Audio{ 65 | FileID: dbReadyFile.ID, 66 | Ready: true, 67 | }). 68 | Count(&audioAmount); res.Error != nil { 69 | log.Printf("Failed to count audio by (delete candidate): Searcher ID %d inside database. Error: %v", dbReadyFile.ID, res.Error) 70 | continue 71 | } 72 | 73 | // in case all qualitys are encoded or failed the original file can be deleted 74 | if qualityAmount == int64(len(dbReadyFile.Qualitys)) && 75 | subtitleAmount == int64(len(dbReadyFile.Subtitles)) && 76 | audioAmount == int64(len(dbReadyFile.Audios)) { 77 | if err := os.Remove(dbReadyFile.Path); err != nil { 78 | log.Printf("Failed to delete file from path (%v): %v", dbReadyFile.Path, err) 79 | continue 80 | } 81 | 82 | // overwrite total filesize in file 83 | newSize, err := helpers.DirSize(dbReadyFile.Folder) 84 | if err != nil { 85 | log.Printf("Failed to calc folder size after cleenup: %v", err) 86 | } 87 | dbReadyFile.Size = newSize 88 | dbReadyFile.Path = "" 89 | inits.DB.Save(&dbReadyFile) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /services/Resources.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "ch/kirari04/videocms/config" 5 | "ch/kirari04/videocms/inits" 6 | "ch/kirari04/videocms/models" 7 | "log" 8 | "time" 9 | 10 | "github.com/shirou/gopsutil/v3/cpu" 11 | "github.com/shirou/gopsutil/v3/disk" 12 | "github.com/shirou/gopsutil/v3/mem" 13 | "github.com/shirou/gopsutil/v3/net" 14 | ) 15 | 16 | var resourcesInterval = time.Second * 10 17 | var netSent uint64 = 0 18 | var netRecv uint64 = 0 19 | 20 | var diskWrite uint64 = 0 21 | var diskRead uint64 = 0 22 | 23 | func Resources() { 24 | go func() { 25 | // delete stats older than 24h 26 | for { 27 | time.Sleep(time.Minute * 1) 28 | if res := inits.DB. 29 | Where("created_at < ?", time.Now().Add(time.Hour*24*-1)). 30 | Unscoped(). 31 | Delete(&models.SystemResource{}); res.Error != nil { 32 | log.Println("Failed to delete system resources") 33 | } 34 | } 35 | }() 36 | for { 37 | v, _ := mem.VirtualMemory() 38 | c, _ := cpu.Percent(time.Second*2, false) 39 | n, _ := net.IOCounters(false) 40 | d, _ := disk.IOCounters(config.ENV.StatsDriveName) 41 | 42 | printCpu := c[0] 43 | printRam := v.UsedPercent 44 | 45 | var printNetSent uint64 = 0 46 | if netSent == 0 { 47 | netSent = n[0].BytesSent 48 | } else { 49 | printNetSent = n[0].BytesSent - netSent 50 | netSent = n[0].BytesSent 51 | } 52 | 53 | var printNetRecv uint64 = 0 54 | if netRecv == 0 { 55 | netRecv = n[0].BytesRecv 56 | } else { 57 | printNetRecv = n[0].BytesRecv - netRecv 58 | netRecv = n[0].BytesRecv 59 | } 60 | 61 | var printDiskWrite uint64 = 0 62 | if diskWrite == 0 { 63 | diskWrite = d[config.ENV.StatsDriveName].WriteBytes 64 | } else { 65 | printDiskWrite = d[config.ENV.StatsDriveName].WriteBytes - diskWrite 66 | diskWrite = d[config.ENV.StatsDriveName].WriteBytes 67 | } 68 | 69 | var printDiskRead uint64 = 0 70 | if diskRead == 0 { 71 | diskRead = d[config.ENV.StatsDriveName].ReadBytes 72 | } else { 73 | printDiskRead = d[config.ENV.StatsDriveName].ReadBytes - diskRead 74 | diskRead = d[config.ENV.StatsDriveName].ReadBytes 75 | } 76 | 77 | var printENCQualityQueue int64 78 | if res := inits.DB.Model(&models.Quality{}). 79 | Where(&models.Quality{ 80 | Ready: false, 81 | Failed: false, 82 | }, "Ready", "Failed"). 83 | Count(&printENCQualityQueue); res.Error != nil { 84 | log.Println("Failed to count printENCQualityQueue", res.Error) 85 | } 86 | var printENCAudioQueue int64 87 | if res := inits.DB.Model(&models.Audio{}). 88 | Where(&models.Audio{ 89 | Ready: false, 90 | Failed: false, 91 | }, "Ready", "Failed"). 92 | Count(&printENCAudioQueue); res.Error != nil { 93 | log.Println("Failed to count printENCAudioQueue", res.Error) 94 | } 95 | var printENCSubtitleQueue int64 96 | if res := inits.DB.Model(&models.Subtitle{}). 97 | Where(&models.Subtitle{ 98 | Ready: false, 99 | Failed: false, 100 | }, "Ready", "Failed"). 101 | Count(&printENCSubtitleQueue); res.Error != nil { 102 | log.Println("Failed to count printENCSubtitleQueue", res.Error) 103 | } 104 | 105 | if res := inits.DB.Create(&models.SystemResource{ 106 | Cpu: printCpu, 107 | Mem: printRam, 108 | NetOut: printNetSent / uint64(resourcesInterval.Seconds()), 109 | NetIn: printNetRecv / uint64(resourcesInterval.Seconds()), 110 | DiskW: printDiskWrite / uint64(resourcesInterval.Seconds()), 111 | DiskR: printDiskRead / uint64(resourcesInterval.Seconds()), 112 | ENCQualityQueue: printENCQualityQueue, 113 | ENCAudioQueue: printENCAudioQueue, 114 | ENCSubtitleQueue: printENCSubtitleQueue, 115 | }); res.Error != nil { 116 | log.Println("Failed to save system resources", res.Error) 117 | } 118 | time.Sleep(resourcesInterval) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/files/test1.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/test/files/test1.mkv -------------------------------------------------------------------------------- /tmp/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirari04/videocms/a8bb6b03783937b03cb51fbe111fead39f0f97ba/tmp/main -------------------------------------------------------------------------------- /views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |14 | {{.Error}} 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |